← Kursa Dön
📄 Text · 30 min

Jest ile Unit Test

Jest: JavaScript Test Çerçevesi

Önceki derste test neden yazılır, hangi türleri vardır bunları öğrendin. Şimdi ellerini kirletme zamanı — gerçek test yazacağız. JavaScript dünyasının en popüler test framework'ü Jest'i kullanacağız.

Jest, Meta (eski adıyla Facebook) tarafından geliştirildi. React ile birlikte doğdu ama sadece React için değil — herhangi bir JavaScript/TypeScript projesinde kullanabilirsin. Jest'i özel kılan şey: test runner, assertion kütüphanesi, mocking sistemi, code coverage — hepsi tek pakette. Ekstra kurulum gerektirmez, kutudan çıkar çıkmaz çalışır.


Kurulum ve Yapılandırma

# Temel kurulum
npm install -D jest

# TypeScript desteği
npm install -D jest ts-jest @types/jest typescript

# ESM modül desteği (opsiyonel)
npm install -D @jest/globals

Jest Yapılandırması

// jest.config.js
module.exports = {
  // TypeScript desteği
  preset: "ts-jest",

  // Test ortamı
  testEnvironment: "node", // veya "jsdom" (tarayıcı API'leri için)

  // Test dosyası pattern'ları
  testMatch: [
    "**/__tests__/**/*.(ts|js)",
    "**/*.(test|spec).(ts|js)",
  ],

  // Coverage ayarları
  collectCoverageFrom: [
    "src/**/*.{ts,js}",
    "!src/**/*.d.ts",
    "!src/**/index.ts",
  ],

  // Path alias (tsconfig paths ile uyumlu)
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
  },

  // Kurulum dosyası
  setupFilesAfterFramework: ["./jest.setup.js"],
};
// package.json scripts
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:verbose": "jest --verbose"
  }
}

describe, it ve test: Test Yapısı

Jest'te testler gruplara ayrılır. describe bir grup oluşturur, test (veya it) tek bir test case tanımlar:

// describe: Mantıksal gruplama
// test/it: Tek bir test senaryosu (ikisi aynı şey)
describe("Calculator", () => {
  describe("add metodu", () => {
    test("iki pozitif sayıyı toplar", () => {
      const calc = new Calculator();
      expect(calc.add(2, 3)).toBe(5);
    });

    test("negatif sayılarla çalışır", () => {
      const calc = new Calculator();
      expect(calc.add(-1, -2)).toBe(-3);
    });

    it("ondalıklı sayıları doğru toplar", () => {
      const calc = new Calculator();
      expect(calc.add(0.1, 0.2)).toBeCloseTo(0.3);
    });
  });

  describe("divide metodu", () => {
    test("sıfıra bölme hata fırlatır", () => {
      const calc = new Calculator();
      expect(() => calc.divide(10, 0)).toThrow("Division by zero");
    });
  });
});

Test Yaşam Döngüsü: Setup ve Teardown

Her test izole olmalı — bir testin sonucu diğerini etkilememeli. beforeEach, afterEach, beforeAll, afterAll ile ortak kurulum ve temizlik yaparız:

describe("UserService", () => {
  let db;
  let userService;

  // Tüm testlerden ÖNCE bir kez çalışır
  beforeAll(async () => {
    db = await connectToTestDatabase();
  });

  // Her testten ÖNCE çalışır
  beforeEach(async () => {
    await db.clear(); // Veritabanını temizle
    userService = new UserService(db);
  });

  // Her testten SONRA çalışır
  afterEach(() => {
    jest.restoreAllMocks(); // Mock'ları sıfırla
  });

  // Tüm testlerden SONRA bir kez çalışır
  afterAll(async () => {
    await db.disconnect();
  });

  test("kullanıcı oluşturur", async () => {
    const user = await userService.create({ name: "Ali" });
    expect(user.id).toBeDefined();
    expect(user.name).toBe("Ali");
  });

  test("var olan email ile kayıt hata verir", async () => {
    await userService.create({ name: "Ali", email: "ali@test.com" });

    await expect(
      userService.create({ name: "Veli", email: "ali@test.com" })
    ).rejects.toThrow("Email zaten kayıtlı");
  });
});
Çalışma sırası:
  beforeAll          ← 1 kez
  ├── beforeEach     ← test 1 öncesi
  │   └── test 1
  │   └── afterEach  ← test 1 sonrası
  ├── beforeEach     ← test 2 öncesi
  │   └── test 2
  │   └── afterEach  ← test 2 sonrası
  └── afterAll       ← 1 kez

Matchers: Beklenti İfadeleri

expect() bir değer alır, matcher ise o değerin ne olmasını beklediğini ifade eder. Jest'in zengin matcher koleksiyonu var:

Eşitlik Matchers

// toBe — strict equality (===)
expect(2 + 2).toBe(4);
expect("hello").toBe("hello");

// toEqual — deep equality (nesneler için)
expect({ name: "Ali", age: 25 }).toEqual({ name: "Ali", age: 25 });
// toBe ile bu BAŞARISIZ olur — farklı referanslar!
expect({ a: 1 }).not.toBe({ a: 1 }); // true!
expect({ a: 1 }).toEqual({ a: 1 });  // true! ✅

// toStrictEqual — daha katı deep equality (undefined property'ler dahil)
expect({ a: 1, b: undefined }).not.toStrictEqual({ a: 1 });
expect({ a: 1, b: undefined }).toEqual({ a: 1 }); // Bu geçer!

⚠️ Dikkat: toBe referans karşılaştırması yapar. Nesneler ve diziler için toEqual kullan. { a: 1 } === { a: 1 } false'tur çünkü farklı bellekte yaşarlar.

Truthiness Matchers

// null, undefined, truthy, falsy kontrolleri
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect("hello").toBeDefined();

expect(true).toBeTruthy();
expect(1).toBeTruthy();
expect("text").toBeTruthy();

expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect("").toBeFalsy();
expect(null).toBeFalsy();

Sayısal Matchers

expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(5).toBeLessThanOrEqual(5);

// Kayan nokta aritmetiği için
expect(0.1 + 0.2).toBeCloseTo(0.3); // ✅
expect(0.1 + 0.2).toBe(0.3);        // ❌ 0.30000000000000004

String Matchers

expect("Merhaba Dünya").toContain("Dünya");
expect("hello@test.com").toMatch(/^[^\s@]+@[^\s@]+$/); // Regex
expect("TypeScript").toMatch("Script");
expect("error").toHaveLength(5);

Array ve Object Matchers

const shoppingList = ["süt", "ekmek", "yumurta", "peynir"];

expect(shoppingList).toContain("ekmek");
expect(shoppingList).toHaveLength(4);
expect(shoppingList).toEqual(expect.arrayContaining(["süt", "yumurta"]));

// Object
const user = { name: "Ali", age: 25, email: "ali@test.com" };
expect(user).toHaveProperty("name");
expect(user).toHaveProperty("name", "Ali");
expect(user).toMatchObject({ name: "Ali", age: 25 }); // Kısmi eşleşme
expect(user).toEqual(expect.objectContaining({ name: "Ali" }));

Hata Matchers

// Fonksiyonun hata fırlatmasını bekle
function riskyOperation() {
  throw new Error("Bir şeyler yanlış gitti");
}

expect(() => riskyOperation()).toThrow();
expect(() => riskyOperation()).toThrow("yanlış gitti"); // Mesaj içeriği
expect(() => riskyOperation()).toThrow(Error);           // Hata tipi
expect(() => riskyOperation()).toThrow(/yanlış/);        // Regex

// NOT: Fonksiyonu arrow function içinde çağır!
// ❌ expect(riskyOperation()).toThrow();  // Bu ÇALIŞMAZ
// ✅ expect(() => riskyOperation()).toThrow();

.not Modifier

// Herhangi bir matcher'ı tersine çevir
expect(5).not.toBe(3);
expect([1, 2, 3]).not.toContain(4);
expect("hello").not.toMatch(/world/);
expect({ a: 1 }).not.toBeNull();

Mocking: Dış Bağımlılıkları Taklit Etme

Test yazarken dış bağımlılıklar sorun yaratır: API çağrıları, veritabanı, dosya sistemi, tarih/zaman... Bunları gerçekten kullanmak testleri yavaşlatır, kırılgan yapar ve izolasyonu bozar.

Mock'lar bu bağımlılıkları taklit eder — gerçekmişçesine davranır ama kontrol altındadır.

Bir film setindeki sahte şehir gibi düşün: dışarıdan gerçek görünür ama arkasında karton var. Test senaryosu için yeterli.

jest.fn() — Mock Fonksiyon

// Boş mock fonksiyon oluştur
const mockCallback = jest.fn();

// Çağrıldı mı?
mockCallback("Ali", 25);
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith("Ali", 25);

// Dönüş değeri ayarla
const mockFetch = jest.fn()
  .mockReturnValue("varsayılan")           // Her zaman
  .mockReturnValueOnce("ilk çağrı")       // Sadece ilk çağrıda
  .mockReturnValueOnce("ikinci çağrı");    // Sadece ikinci çağrıda

console.log(mockFetch()); // "ilk çağrı"
console.log(mockFetch()); // "ikinci çağrı"
console.log(mockFetch()); // "varsayılan"

jest.mock() — Modül Mocking

// api.js
export async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// userService.js
import { fetchUser } from "./api";

export async function getUserDisplayName(id) {
  const user = await fetchUser(id);
  return `${user.firstName} ${user.lastName}`;
}
// userService.test.js
import { getUserDisplayName } from "./userService";
import { fetchUser } from "./api";

// Modülü tamamen mock'la
jest.mock("./api");

// TypeScript tip güvenliği için
const mockedFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>;

describe("getUserDisplayName", () => {
  beforeEach(() => {
    jest.clearAllMocks(); // Her testten önce mock'ları temizle
  });

  test("kullanıcı adı ve soyadını birleştirir", async () => {
    // Mock'un ne döndüreceğini ayarla
    mockedFetchUser.mockResolvedValue({
      firstName: "Ali",
      lastName: "Yılmaz",
    });

    const displayName = await getUserDisplayName(1);

    expect(displayName).toBe("Ali Yılmaz");
    expect(mockedFetchUser).toHaveBeenCalledWith(1);
  });

  test("API hatası durumunda hata fırlatır", async () => {
    mockedFetchUser.mockRejectedValue(new Error("API Error"));

    await expect(getUserDisplayName(1)).rejects.toThrow("API Error");
  });
});

jest.spyOn() — Mevcut Fonksiyonu İzle

// Fonksiyonu değiştirmeden izle (casusluk yap)
const user = {
  getName() { return "Ali"; },
  greet() { return `Merhaba, ${this.getName()}`; },
};

const spy = jest.spyOn(user, "getName");

user.greet();

expect(spy).toHaveBeenCalled();
expect(spy).toHaveReturnedWith("Ali");

// İzleme + davranış değiştirme
jest.spyOn(user, "getName").mockReturnValue("Veli");
expect(user.greet()).toBe("Merhaba, Veli");

spy.mockRestore(); // Orijinal davranışa dön

Timer Mocking

// setTimeout, setInterval mock'lama
function delayedGreet(name, callback) {
  setTimeout(() => {
    callback(`Merhaba ${name}`);
  }, 3000);
}

test("3 saniye sonra selamlama callback'ini çağırır", () => {
  jest.useFakeTimers(); // Zamanlayıcıları mock'la
  const callback = jest.fn();

  delayedGreet("Ali", callback);

  // Zaman henüz ilerlemedi
  expect(callback).not.toHaveBeenCalled();

  // 3 saniye ilerlet
  jest.advanceTimersByTime(3000);

  expect(callback).toHaveBeenCalledWith("Merhaba Ali");

  jest.useRealTimers(); // Gerçek zamanlayıcılara dön
});

Date Mocking

test("bugünün tarihini döndürür", () => {
  // Sabit tarih ayarla
  const fixedDate = new Date("2025-03-01T10:00:00Z");
  jest.useFakeTimers().setSystemTime(fixedDate);

  const result = getCurrentDate(); // Fonksiyonun new Date() kullandığını varsay

  expect(result).toBe("2025-03-01");

  jest.useRealTimers();
});

Async Test

Asenkron kod test etmek için Jest birden fazla yol sunar:

async/await

// En temiz ve önerilen yol
test("kullanıcı verisi getirir", async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe("Ali");
});

// Hata testi
test("olmayan kullanıcı hata fırlatır", async () => {
  await expect(fetchUser(999)).rejects.toThrow("Not found");
});

.resolves / .rejects

// Alternatif syntax — okunabilirlik için
test("başarılı promise", () => {
  return expect(fetchUser(1)).resolves.toEqual({ name: "Ali" });
});

test("başarısız promise", () => {
  return expect(fetchUser(999)).rejects.toThrow();
});

Callback Tabanlı (done)

// Eski stil — callback kullanan kod için
test("veri yüklendiğinde callback çağrılır", (done) => {
  loadData((error, data) => {
    try {
      expect(error).toBeNull();
      expect(data).toBeDefined();
      done(); // Test tamamlandı — bunu çağırmazsan timeout olur!
    } catch (e) {
      done(e); // Hata varsa done'a geç
    }
  });
});

⚠️ Dikkat: Async testlerde return veya await kullanmayı unutma! Aksi halde test, Promise resolve olmadan başarılı görünür:

>

```javascript // ❌ Yanlış — test hemen başarılı olur, Promise beklemez test("kullanıcı getirir", () => { fetchUser(1).then(user => { expect(user.name).toBe("Ali"); // Bu hiç çalışmayabilir! }); });

>

// ✅ Doğru — await ile bekle test("kullanıcı getirir", async () => { const user = await fetchUser(1); expect(user.name).toBe("Ali"); }); ```


Snapshot Testing

Snapshot testi, bir değerin önceki halinin fotoğrafını çeker ve her çalıştırıldığında o fotoğrafla karşılaştırır. UI bileşenleri, büyük nesneler, API yanıtları gibi yapıları test etmek için kullanılır.

// İlk çalıştırıldığında snapshot dosyası oluşturur
// Sonraki çalıştırmalarda karşılaştırır
test("kullanıcı nesnesinin yapısı değişmemeli", () => {
  const user = createUser({ name: "Ali", email: "ali@test.com" });

  expect(user).toMatchSnapshot();
  // __snapshots__/user.test.js.snap dosyası oluşur:
  // exports[`kullanıcı nesnesinin yapısı değişmemeli 1`] = `
  // {
  //   "name": "Ali",
  //   "email": "ali@test.com",
  //   "id": Any<String>,
  //   "createdAt": Any<Date>,
  // }
  // `;
});

// Inline snapshot — dosya yerine test içinde sakla
test("hata mesajı formatı", () => {
  const error = formatError("Geçersiz email");

  expect(error).toMatchInlineSnapshot(`
    {
      "code": "VALIDATION_ERROR",
      "message": "Geçersiz email",
      "timestamp": Any<String>,
    }
  `);
});

Snapshot Güncelleme

# Snapshot'ları güncelle (yapı kasıtlı olarak değiştiyse)
jest --updateSnapshot
# veya
jest -u

💡 İpucu: Snapshot testlerini bilinçli kullan. Her şeyi snapshot'lamak, her değişiklikte snapshot güncelleme zorunluluğu yaratır ve testlerin anlamını kaybettirir. Büyük, karmaşık çıktılar için ideal — basit değerler için toBe/toEqual kullan.


Gerçek Dünya Örneği: Shopping Cart Test Suite

// cart.ts
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

class ShoppingCart {
  private items: CartItem[] = [];

  addItem(item: Omit<CartItem, "quantity">, quantity: number = 1): void {
    if (quantity <= 0) throw new Error("Miktar pozitif olmalı");

    const existing = this.items.find(i => i.id === item.id);
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.push({ ...item, quantity });
    }
  }

  removeItem(id: string): void {
    this.items = this.items.filter(item => item.id !== id);
  }

  getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  get itemCount(): number {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }

  getItems(): readonly CartItem[] {
    return [...this.items];
  }

  clear(): void {
    this.items = [];
  }

  applyDiscount(percent: number): number {
    if (percent < 0 || percent > 100) throw new Error("Geçersiz indirim");
    const total = this.getTotal();
    return total - (total * percent / 100);
  }
}

export { ShoppingCart, CartItem };
// cart.test.ts
import { ShoppingCart } from "./cart";

describe("ShoppingCart", () => {
  let cart: ShoppingCart;

  beforeEach(() => {
    cart = new ShoppingCart();
  });

  describe("addItem", () => {
    test("sepete ürün ekler", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 5000 });

      expect(cart.itemCount).toBe(1);
      expect(cart.getItems()[0]).toEqual({
        id: "1", name: "Laptop", price: 5000, quantity: 1,
      });
    });

    test("aynı ürün tekrar eklendiğinde miktarı artırır", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 5000 });
      cart.addItem({ id: "1", name: "Laptop", price: 5000 }, 2);

      expect(cart.itemCount).toBe(3);
      expect(cart.getItems()).toHaveLength(1); // Tek ürün, 3 adet
    });

    test("sıfır veya negatif miktar hata fırlatır", () => {
      expect(() =>
        cart.addItem({ id: "1", name: "Laptop", price: 5000 }, 0)
      ).toThrow("Miktar pozitif olmalı");

      expect(() =>
        cart.addItem({ id: "1", name: "Laptop", price: 5000 }, -1)
      ).toThrow("Miktar pozitif olmalı");
    });
  });

  describe("removeItem", () => {
    test("ürünü sepetten kaldırır", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 5000 });
      cart.addItem({ id: "2", name: "Mouse", price: 200 });

      cart.removeItem("1");

      expect(cart.itemCount).toBe(1);
      expect(cart.getItems()[0].name).toBe("Mouse");
    });

    test("olmayan ürünü silmek hata vermez", () => {
      expect(() => cart.removeItem("nonexistent")).not.toThrow();
    });
  });

  describe("getTotal", () => {
    test("toplam fiyatı doğru hesaplar", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 5000 });
      cart.addItem({ id: "2", name: "Mouse", price: 200 }, 2);

      expect(cart.getTotal()).toBe(5400); // 5000 + 200*2
    });

    test("boş sepet için sıfır döndürür", () => {
      expect(cart.getTotal()).toBe(0);
    });
  });

  describe("applyDiscount", () => {
    test("yüzde indirimi uygular", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 1000 });

      expect(cart.applyDiscount(20)).toBe(800);
    });

    test("geçersiz indirim yüzdesi hata fırlatır", () => {
      expect(() => cart.applyDiscount(-10)).toThrow("Geçersiz indirim");
      expect(() => cart.applyDiscount(110)).toThrow("Geçersiz indirim");
    });

    test("%0 indirim fiyatı değiştirmez", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 1000 });
      expect(cart.applyDiscount(0)).toBe(1000);
    });

    test("%100 indirim sıfır döndürür", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 1000 });
      expect(cart.applyDiscount(100)).toBe(0);
    });
  });

  describe("clear", () => {
    test("sepeti tamamen boşaltır", () => {
      cart.addItem({ id: "1", name: "Laptop", price: 5000 });
      cart.addItem({ id: "2", name: "Mouse", price: 200 });

      cart.clear();

      expect(cart.itemCount).toBe(0);
      expect(cart.getTotal()).toBe(0);
      expect(cart.getItems()).toEqual([]);
    });
  });
});
# Test çalıştır
npm test -- cart.test.ts

# Çıktı:
# PASS  src/cart.test.ts
#   ShoppingCart
#     addItem
#       ✓ sepete ürün ekler (3ms)
#       ✓ aynı ürün tekrar eklendiğinde miktarı artırır (1ms)
#       ✓ sıfır veya negatif miktar hata fırlatır
#     removeItem
#       ✓ ürünü sepetten kaldırır
#       ✓ olmayan ürünü silmek hata vermez
#     getTotal
#       ✓ toplam fiyatı doğru hesaplar
#       ✓ boş sepet için sıfır döndürür
#     applyDiscount
#       ✓ yüzde indirimi uygular
#       ✓ geçersiz indirim yüzdesi hata fırlatır (1ms)
#       ✓ %0 indirim fiyatı değiştirmez
#       ✓ %100 indirim sıfır döndürür
#     clear
#       ✓ sepeti tamamen boşaltır
#
# Tests: 12 passed, 12 total

Özet

  • describe/test(it): Testleri grupla ve tanımla. describe mantıksal grup, test tek senaryo.

  • beforeEach/afterEach: Her testten önce/sonra çalışır — izolasyon sağlar.

  • Matchers: toBe (strict equality), toEqual (deep), toContain, toThrow, toBeCloseTo vb.

  • jest.fn(): Mock fonksiyon oluşturur — çağrı sayısı, parametreler, dönüş değeri kontrol edilir.

  • jest.mock(): Tüm modülü mock'lar — API, veritabanı gibi dış bağımlılıklar izole edilir.

  • jest.spyOn(): Mevcut fonksiyonu izler veya geçici olarak davranışını değiştirir.

  • Async test: async/await ile veya .resolves/.rejects ile.

  • Snapshot: Büyük yapıların "fotoğrafını" çeker, değişiklik olursa uyarır.