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/globalsJest 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 kezMatchers: 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:
toBereferans karşılaştırması yapar. Nesneler ve diziler içintoEqualkullan.{ 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.30000000000000004String 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önTimer 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
returnveyaawaitkullanmayı 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/toEqualkullan.
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.
describemantıksal grup,testtek senaryo.beforeEach/afterEach: Her testten önce/sonra çalışır — izolasyon sağlar.
Matchers:
toBe(strict equality),toEqual(deep),toContain,toThrow,toBeCloseTovb.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/awaitile veya.resolves/.rejectsile.Snapshot: Büyük yapıların "fotoğrafını" çeker, değişiklik olursa uyarır.
AI Asistan
Sorularını yanıtlamaya hazır