Test Best Practices
İyi Test Nedir?
Önceki derslerde Jest'in araçlarını öğrendin — describe, expect, jest.fn(), jest.mock(). Ama araçları bilmek yetmez; bir boya fırçası ve tuval vermekle ressam olunmaz. Bu derste nasıl iyi test yazılır sorusunun cevabını arayacağız.
İyi bir test: okunabilir, güvenilir, hızlı ve bakımı kolay olmalı. Kötü testler — yavaş, kırılgan, anlaşılmaz testler — test yazmaktan daha kötüdür çünkü yanlış güven verir ve geliştirme hızını düşürür.
Analoji: Test yazmak bahçe bakımına benzer. İyi bakılan bir bahçe zamanla güzelleşir, meyve verir. Bakımsız bahçe ise yabani otlarla dolar ve sonunda terk edersin. İyi test suite'i de düzenli bakım ve doğru pratiklerle güçlenir. Kötü testler ise projeyi "test yazmak zaman kaybı" duvarına çarptırır.
AAA Pattern: Arrange-Act-Assert
Her iyi testin üç bölümü vardır. Bunu bir bilimsel deney gibi düşün:
Arrange (Hazırla) — Deney ortamını kur
Act (Uygula) — Deneyi yap
Assert (Doğrula) — Sonucu kontrol et
test("sepete ürün eklendiğinde toplam fiyat güncellenir", () => {
// Arrange — Gerekli nesneleri oluştur, başlangıç durumunu kur
const cart = new ShoppingCart();
const laptop = { id: "1", name: "Laptop", price: 5000 };
const mouse = { id: "2", name: "Mouse", price: 200 };
// Act — Test edilen eylemi gerçekleştir
cart.addItem(laptop);
cart.addItem(mouse, 2);
// Assert — Sonucu doğrula
expect(cart.getTotal()).toBe(5400);
expect(cart.itemCount).toBe(3);
});Neden AAA?
Bu pattern testi okunabilir yapar. Birisi testine baktığında hemen anlayabilir:
Ne kuruldu?
Ne yapıldı?
Ne bekleniyor?
// ❌ Kötü: AAA karışık, ne test edildiği belirsiz
test("kullanıcı işlemleri", () => {
const user = createUser("Ali");
expect(user.name).toBe("Ali");
user.updateEmail("ali@new.com");
expect(user.email).toBe("ali@new.com");
const found = findUser(user.id);
expect(found).toEqual(user);
deleteUser(user.id);
expect(findUser(user.id)).toBeNull();
});
// ✅ İyi: Her test tek bir davranışı test eder
test("kullanıcı oluşturulduğunda isim atanır", () => {
const user = createUser("Ali");
expect(user.name).toBe("Ali");
});
test("email güncelleme doğru çalışır", () => {
const user = createUser("Ali");
user.updateEmail("ali@new.com");
expect(user.email).toBe("ali@new.com");
});
test("silinen kullanıcı bulunamaz", () => {
const user = createUser("Ali");
deleteUser(user.id);
expect(findUser(user.id)).toBeNull();
});💡 İpucu: Her test tek bir davranışı test etmeli. "Ve" kelimesi test isminde geçiyorsa, muhtemelen birden fazla şeyi test ediyorsundur — ayır.
Test Doubles: Mock, Stub, Spy ve Fake
"Test double" Hollywood'daki dublör kavramından gelir — gerçek aktör yerine geçen, benzer görünen kişi. Yazılımda da gerçek bağımlılık yerine geçen nesnelere test double denir. Beş türü var:
Dummy — Sadece Var Olmak İçin
Hiçbir şey yapmaz, sadece parametre listesini doldurmak için kullanılır:
test("log fonksiyonu hata yazmaz", () => {
const dummyLogger = {}; // Hiçbir metodu çağrılmayacak
// Logger parametre olarak gerekli ama bu testte kullanılmıyor
const service = new UserService(db, dummyLogger);
const user = service.findById(1);
expect(user).toBeDefined();
});Stub — Sabit Cevap Veren
Çağrıldığında önceden belirlenmiş cevabı döndürür. Dış bağımlılığın davranışını simüle eder:
test("kullanıcı bulunamazsa null döndürür", async () => {
// Stub: her zaman null döndüren veritabanı
const dbStub = {
findById: jest.fn().mockResolvedValue(null),
};
const service = new UserService(dbStub);
const user = await service.getUser(999);
expect(user).toBeNull();
});
test("kullanıcı bulunursa formatlanmış isim döndürür", async () => {
// Stub: belirli bir kullanıcı döndüren veritabanı
const dbStub = {
findById: jest.fn().mockResolvedValue({
firstName: "Ali",
lastName: "Yılmaz",
}),
};
const service = new UserService(dbStub);
const name = await service.getDisplayName(1);
expect(name).toBe("Ali Yılmaz");
});Spy — Gizlice İzleyen
Gerçek fonksiyonu çalıştırır ama çağrıları kaydeder — ne zaman, hangi parametrelerle çağrıldığını takip eder:
test("ürün eklendiğinde analytics'e event gönderilir", () => {
const analytics = { trackEvent: jest.fn() }; // Spy
const cart = new ShoppingCart(analytics);
cart.addItem({ id: "1", name: "Laptop", price: 5000 });
// Spy'ın kayıtlarını kontrol et
expect(analytics.trackEvent).toHaveBeenCalledTimes(1);
expect(analytics.trackEvent).toHaveBeenCalledWith("item_added", {
itemId: "1",
itemName: "Laptop",
});
});
// jest.spyOn — mevcut nesnenin metodunu izle
test("Date.now mock'lanabilir", () => {
const fixedTime = new Date("2025-01-01").getTime();
jest.spyOn(Date, "now").mockReturnValue(fixedTime);
const result = getTimestamp();
expect(result).toBe(fixedTime);
jest.restoreAllMocks(); // Orijinal davranışı geri yükle
});Mock — Beklentileri Olan Taklit
Stub + doğrulama: hem sabit cevap verir hem de doğru şekilde çağrıldığını kontrol eder:
test("sipariş onaylandığında email gönderilir", async () => {
// Mock: hem cevap verir hem çağrıyı doğrular
const emailService = {
sendEmail: jest.fn().mockResolvedValue(true),
};
const orderService = new OrderService(emailService);
await orderService.confirmOrder({ id: "1", email: "ali@test.com" });
// Mock doğrulaması
expect(emailService.sendEmail).toHaveBeenCalledWith(
"ali@test.com",
expect.stringContaining("Sipariş Onayı"),
expect.any(String) // Body — içeriği bu testte önemli değil
);
});Fake — Basitleştirilmiş Gerçek
Gerçek implementasyonun basitleştirilmiş versiyonu. In-memory veritabanı buna güzel bir örnek:
// Fake veritabanı — gerçek gibi davranır ama bellekte çalışır
class FakeUserRepository {
private users = new Map();
private nextId = 1;
async create(data) {
const user = { ...data, id: this.nextId++ };
this.users.set(user.id, user);
return user;
}
async findById(id) {
return this.users.get(id) || null;
}
async findByEmail(email) {
return [...this.users.values()].find(u => u.email === email) || null;
}
async delete(id) {
return this.users.delete(id);
}
}
// Test
test("kullanıcı CRUD işlemleri", async () => {
const fakeRepo = new FakeUserRepository();
const service = new UserService(fakeRepo);
// Create
const user = await service.createUser({ name: "Ali", email: "ali@test.com" });
expect(user.id).toBeDefined();
// Read
const found = await service.getUser(user.id);
expect(found.name).toBe("Ali");
// Delete
await service.deleteUser(user.id);
const deleted = await service.getUser(user.id);
expect(deleted).toBeNull();
});Ne Zaman Hangisi?
Dummy → Parametre doldurmak için, test ettiğin şeyle ilgisiz
Stub → Dış servisin cevabını kontrol etmek istediğinde
Spy → Bir fonksiyonun çağrılıp çağrılmadığını doğrulamak istediğinde
Mock → Hem cevap hem çağrı doğrulaması gerektiğinde
Fake → Gerçek implementasyona yakın, ama hafif bir alternatif gerektiğindeİmplementasyon Detayı vs Davranış
En yaygın test hatalarından biri: kodun nasıl yaptığını test etmek yerine ne yaptığını test etmek.
// ❌ İmplementasyon detayı test ediliyor
test("kullanıcı listesi sıralanır", () => {
const users = [{ name: "Can" }, { name: "Ali" }, { name: "Berk" }];
const sorted = sortUsers(users);
// Bu test, sort algoritmasının Array.sort kullandığını test ediyor
// Yarın quicksort'a geçersen test kırılır — ama sonuç aynı!
expect(Array.prototype.sort).toHaveBeenCalled();
});
// ✅ Davranış test ediliyor
test("kullanıcı listesi isme göre A-Z sıralanır", () => {
const users = [{ name: "Can" }, { name: "Ali" }, { name: "Berk" }];
const sorted = sortUsers(users);
expect(sorted).toEqual([
{ name: "Ali" },
{ name: "Berk" },
{ name: "Can" },
]);
});// ❌ İç state'i test etme
test("counter state'i güncellenir", () => {
const counter = new Counter();
counter.increment();
// İç state'e erişim — implementasyon değişirse kırılır
expect(counter._count).toBe(1); // ❌ Private state
expect(counter.state.value).toBe(1); // ❌ İç yapı
});
// ✅ Public API'yi test et
test("increment sonrası getValue doğru değer döndürür", () => {
const counter = new Counter();
counter.increment();
expect(counter.getValue()).toBe(1); // ✅ Public method
expect(counter.toString()).toBe("1"); // ✅ Public method
});💡 İpucu: Kendine sor: "Kodu refactor etsem ama davranışı değiştirmesem, bu test kırılır mı?" Cevap evet ise, implementasyon detayını test ediyorsun — testi düzelt.
Code Coverage: Ne Kadar Yeterli?
Coverage, kodunun yüzde kaçının testlerle kapsandığını gösterir. Dört ana metrici var:
# Coverage raporu oluştur
jest --coverage--------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files | 87.5 | 75.0 | 90.0 | 87.5 |
cart.ts | 100.0 | 100.0 | 100.0 | 100.0 |
userService.ts | 75.0 | 50.0 | 80.0 | 75.0 |
--------------------|---------|----------|---------|---------|Metrikler:
Statements: Çalıştırılan ifade yüzdesi (her satırdaki her ifade)
Branches: if/else, switch, ternary dallarının test edilme yüzdesi
Functions: Çağrılan fonksiyon yüzdesi
Lines: Test edilen satır yüzdesi
Coverage Hedefleri
// jest.config.js — minimum coverage eşikleri
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
// Kritik modüller için daha yüksek eşik
"./src/payment/": {
branches: 95,
functions: 95,
lines: 95,
},
// Utility fonksiyonları — %100 hedeflenebilir
"./src/utils/": {
branches: 100,
functions: 100,
lines: 100,
},
},
};Coverage Tuzağı
// ❌ %100 coverage ama KÖTÜ test — hiçbir şeyi doğrulamıyor
test("processOrder çağrılabilir", () => {
const result = processOrder({ id: 1, total: 100 });
// Hiç assert yok! Sadece "çağırdım, patlamamış" test edilmiş
// Coverage artıyor ama kalite sıfır
});
// ✅ Anlamlı coverage — davranışı doğrular
test("processOrder toplam 0 ise hata fırlatır", () => {
expect(() => processOrder({ id: 1, total: 0 })).toThrow("Toplam sıfır olamaz");
});
test("processOrder başarılı siparişte order ID döndürür", () => {
const result = processOrder({ id: 1, total: 100 });
expect(result.orderId).toBeDefined();
expect(result.status).toBe("confirmed");
});⚠️ Dikkat: %100 coverage hedefleme. %80-90 genelde yeterli. Coverage sadece "bu satır çalıştırıldı" der, "doğru çalışıyor" demez. Anlamlı assert'ler olmadan yüksek coverage yanlış güven verir. Ödeme, auth, hesaplama gibi kritik modüllerde %95+ hedefle; UI, config gibi modüllerde %70-80 yeterli.
Flaky Test: Güvenilmez Testler
Flaky (kırılgan) test: bazen geçer, bazen geçmez. Aynı kodu, aynı ortamda çalıştırsan bile farklı sonuç verir. Bu testler ekibin teste olan güvenini yok eder — "test kırıldı ama muhtemelen flaky, yoksay" denmeye başlanır ve gerçek hatalar da gözden kaçar.
Yaygın Nedenler ve Çözümler
1. Zaman Bağımlılığı
// ❌ Flaky: Sistem saatine bağımlı
test("yeni kullanıcı bugünün tarihine sahip olmalı", () => {
const user = createUser("Ali");
expect(user.createdAt.toDateString()).toBe(new Date().toDateString());
// Gece yarısına yakın çalışırsa, tarih değişebilir!
});
// ✅ Düzeltme: Zamanı mock'la
test("yeni kullanıcı oluşturulma tarihine sahip olmalı", () => {
const fixedDate = new Date("2025-03-01T12:00:00Z");
jest.useFakeTimers().setSystemTime(fixedDate);
const user = createUser("Ali");
expect(user.createdAt).toEqual(fixedDate);
jest.useRealTimers();
});2. Sıralama Bağımlılığı
// ❌ Flaky: Test A'nın sonucu Test B'yi etkiliyor
let sharedState = [];
test("ürün ekler", () => {
sharedState.push("item1");
expect(sharedState).toHaveLength(1);
});
test("ürün sayısı kontrolü", () => {
// Test A'dan önce çalışırsa: length 0
// Test A'dan sonra çalışırsa: length 1
expect(sharedState).toHaveLength(1); // Sıraya bağlı!
});
// ✅ Düzeltme: Her testte temiz state
describe("ürün işlemleri", () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart(); // Her test temiz başlar
});
test("ürün ekler", () => {
cart.addItem("item1");
expect(cart.itemCount).toBe(1);
});
test("boş sepet kontrolü", () => {
expect(cart.itemCount).toBe(0); // Her zaman 0 — izole
});
});3. Async Race Condition
// ❌ Flaky: Asenkron işlem bitmeden assert
test("veri yüklenir", () => {
loadData();
expect(getData()).toHaveLength(5); // loadData henüz bitmemiş olabilir!
});
// ✅ Düzeltme: async/await ile bekle
test("veri yüklenir", async () => {
await loadData();
expect(getData()).toHaveLength(5);
});
// ✅ Düzeltme: waitFor ile (Testing Library)
test("yükleme sonrası veri gösterilir", async () => {
render(<UserList />);
// Element görünene kadar bekle (polling)
const items = await screen.findAllByRole("listitem");
expect(items).toHaveLength(5);
});4. Rastgele Veri
// ❌ Flaky: Math.random() kullanılıyor
test("rastgele id oluşturur", () => {
const id = generateId();
expect(id).toMatch(/^[a-z]{5}$/); // Bazen sayı içerebilir
});
// ✅ Düzeltme: Random'u mock'la
test("id alfanumerik formatta olmalı", () => {
jest.spyOn(Math, "random").mockReturnValue(0.5);
const id = generateId();
expect(id).toBeDefined();
expect(id.length).toBeGreaterThan(0);
jest.restoreAllMocks();
});5. Ağ Bağımlılığı
// ❌ Flaky: Gerçek API çağrısı — ağ kesintisinde kırılır
test("kullanıcıları getirir", async () => {
const users = await fetch("https://api.example.com/users").then(r => r.json());
expect(users).toHaveLength(10);
// Sunucu yavaşsa? Timeout? Veri değiştiyse? Test kırılır!
});
// ✅ Düzeltme: API'yi mock'la
test("kullanıcıları getirir", async () => {
jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: () => Promise.resolve([
{ id: 1, name: "Ali" },
{ id: 2, name: "Ayşe" },
]),
});
const users = await userService.getUsers();
expect(users).toHaveLength(2);
jest.restoreAllMocks();
});Flaky Test Tespit Etme
# Aynı testi 10 kez çalıştır — flaky mi kontrol et
jest --repeat=10 src/problematic.test.ts
# Testleri rastgele sırada çalıştır — sıra bağımlılığı ara
jest --randomize
# CI'da flaky tespiti: aynı commit'te birden fazla çalıştır
# Farklı sonuç veriyorsa → flakyTest Organizasyonu
Dosya Yapısı
src/
├── services/
│ ├── UserService.ts
│ ├── UserService.test.ts ← Aynı klasörde (yaygın)
│ └── OrderService.ts
├── utils/
│ ├── math.ts
│ └── math.test.ts
└── __tests__/ ← Veya ayrı klasörde
├── integration/
│ └── userFlow.test.ts
└── e2e/
└── login.test.tsDosya isimlendirme konvansiyonları:
*.test.ts → Jest/Vitest varsayılanı
*.spec.ts → Angular geleneği, BDD terminolojisi
__tests__/ → Ayrı klasörde (React Create App geleneği)
# Hangisini seçersen seç, tutarlı ol!Test Kalıpları
// 1. Method-Scenario-Expected (En yaygın)
describe("ShoppingCart", () => {
describe("addItem", () => {
it("should increase item count by 1", () => {});
it("should update total price", () => {});
it("should throw for negative quantity", () => {});
});
describe("removeItem", () => {
it("should decrease item count by 1", () => {});
it("should not go below 0", () => {});
});
});
// 2. Given-When-Then (BDD stili)
describe("UserService", () => {
describe("given: geçerli kullanıcı verisi", () => {
describe("when: createUser çağrıldığında", () => {
it("then: kullanıcı oluşturulur ve ID atanır", async () => {
// ...
});
});
});
});
// 3. Behavior-driven flat (basit projeler için)
describe("Login flow", () => {
it("redirects to dashboard after successful login", () => {});
it("shows error message for invalid credentials", () => {});
it("locks account after 5 failed attempts", () => {});
});Test Data Yönetimi
Factory Pattern — Test Verisi Üretimi
// factories/userFactory.js
function createTestUser(overrides = {}) {
return {
id: Math.random().toString(36).slice(2),
name: "Test User",
email: "test@example.com",
age: 25,
role: "user",
isActive: true,
createdAt: new Date("2025-01-01"),
...overrides, // Override edilmek istenen alanlar
};
}
function createTestOrder(overrides = {}) {
return {
id: Math.random().toString(36).slice(2),
userId: "user-1",
items: [
{ productId: "p1", name: "Laptop", price: 5000, quantity: 1 },
],
total: 5000,
status: "pending",
createdAt: new Date("2025-01-01"),
...overrides,
};
}
// Kullanım — temiz ve okunabilir testler
test("admin kullanıcı tüm siparişleri görebilir", async () => {
const admin = createTestUser({ role: "admin" });
const order1 = createTestOrder({ userId: "u1", total: 100 });
const order2 = createTestOrder({ userId: "u2", total: 200 });
const result = await getVisibleOrders(admin, [order1, order2]);
expect(result).toHaveLength(2);
});
test("normal kullanıcı sadece kendi siparişlerini görebilir", async () => {
const user = createTestUser({ id: "u1", role: "user" });
const myOrder = createTestOrder({ userId: "u1" });
const otherOrder = createTestOrder({ userId: "u2" });
const result = await getVisibleOrders(user, [myOrder, otherOrder]);
expect(result).toHaveLength(1);
expect(result[0].userId).toBe("u1");
});Builder Pattern — Karmaşık Test Verisi
// Daha karmaşık nesneler için Builder pattern
class OrderBuilder {
constructor() {
this.order = {
id: "order-1",
items: [],
status: "pending",
discount: 0,
};
}
withItem(name, price, qty = 1) {
this.order.items.push({ name, price, quantity: qty });
return this; // Method chaining
}
withStatus(status) {
this.order.status = status;
return this;
}
withDiscount(percent) {
this.order.discount = percent;
return this;
}
build() {
const total = this.order.items.reduce(
(sum, i) => sum + i.price * i.quantity, 0
);
return {
...this.order,
total: total * (1 - this.order.discount / 100),
};
}
}
// Kullanım — okunabilir ve anlaşılır
test("indirimli siparişin toplamı doğru hesaplanır", () => {
const order = new OrderBuilder()
.withItem("Laptop", 5000)
.withItem("Mouse", 200, 2)
.withDiscount(10)
.build();
expect(order.total).toBe(4860); // (5000 + 400) * 0.9
});Gerçek Dünya Test Senaryosu: E-Ticaret Sepet
Birden fazla kavramı birleştiren kapsamlı bir test örneği:
// cart.ts — Test edilecek kod
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartDiscount {
code: string;
percent: number;
minTotal: number;
}
class ShoppingCart {
private items: CartItem[] = [];
private discount: CartDiscount | null = null;
addItem(product: { id: string; name: string; price: number }, qty = 1): void {
if (qty <= 0) throw new Error("Miktar pozitif olmalı");
if (product.price < 0) throw new Error("Fiyat negatif olamaz");
const existing = this.items.find(i => i.id === product.id);
if (existing) {
existing.quantity += qty;
} else {
this.items.push({ ...product, quantity: qty });
}
}
removeItem(productId: string): void {
this.items = this.items.filter(i => i.id !== productId);
}
applyDiscount(discount: CartDiscount): boolean {
const subtotal = this.getSubtotal();
if (subtotal < discount.minTotal) return false;
this.discount = discount;
return true;
}
getSubtotal(): number {
return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
}
getTotal(): number {
const subtotal = this.getSubtotal();
if (this.discount) {
return subtotal * (1 - this.discount.percent / 100);
}
return subtotal;
}
get itemCount(): number {
return this.items.reduce((sum, i) => sum + i.quantity, 0);
}
}// cart.test.ts — Kapsamlı testler
describe("ShoppingCart", () => {
let cart;
// Arrange — her test öncesi temiz sepet
beforeEach(() => {
cart = new ShoppingCart();
});
// === Ürün Ekleme ===
describe("addItem", () => {
test("tek ürün eklendiğinde itemCount 1 olur", () => {
cart.addItem({ id: "p1", name: "Laptop", price: 5000 });
expect(cart.itemCount).toBe(1);
});
test("aynı ürün tekrar eklendiğinde miktar artar", () => {
cart.addItem({ id: "p1", name: "Laptop", price: 5000 });
cart.addItem({ id: "p1", name: "Laptop", price: 5000 }, 2);
expect(cart.itemCount).toBe(3); // 1 + 2
});
test("farklı ürünler ayrı item olarak eklenir", () => {
cart.addItem({ id: "p1", name: "Laptop", price: 5000 });
cart.addItem({ id: "p2", name: "Mouse", price: 200 });
expect(cart.itemCount).toBe(2);
});
test("sıfır veya negatif miktar hata fırlatır", () => {
expect(() =>
cart.addItem({ id: "p1", name: "Laptop", price: 5000 }, 0)
).toThrow("Miktar pozitif olmalı");
expect(() =>
cart.addItem({ id: "p1", name: "Laptop", price: 5000 }, -1)
).toThrow("Miktar pozitif olmalı");
});
test("negatif fiyatlı ürün hata fırlatır", () => {
expect(() =>
cart.addItem({ id: "p1", name: "Bug", price: -100 })
).toThrow("Fiyat negatif olamaz");
});
});
// === Ürün Silme ===
describe("removeItem", () => {
test("var olan ürün sepetten kaldırılır", () => {
cart.addItem({ id: "p1", name: "Laptop", price: 5000 });
cart.removeItem("p1");
expect(cart.itemCount).toBe(0);
});
test("olmayan ürünü silmek hata fırlatmaz", () => {
expect(() => cart.removeItem("nonexistent")).not.toThrow();
});
});
// === Fiyat Hesaplama ===
describe("getTotal", () => {
test("boş sepetin toplamı 0", () => {
expect(cart.getTotal()).toBe(0);
});
test("ürünlerin toplam fiyatı doğru hesaplanır", () => {
cart.addItem({ id: "p1", name: "Laptop", price: 5000 });
cart.addItem({ id: "p2", name: "Mouse", price: 200 }, 2);
expect(cart.getTotal()).toBe(5400); // 5000 + 200*2
});
});
// === İndirim ===
describe("applyDiscount", () => {
test("minimum tutarı karşılayan sepete indirim uygulanır", () => {
cart.addItem({ id: "p1", name: "Laptop", price: 5000 });
const applied = cart.applyDiscount({
code: "SALE10", percent: 10, minTotal: 1000,
});
expect(applied).toBe(true);
expect(cart.getTotal()).toBe(4500); // 5000 * 0.9
});
test("minimum tutarı karşılamayan sepete indirim uygulanmaz", () => {
cart.addItem({ id: "p1", name: "Mouse", price: 200 });
const applied = cart.applyDiscount({
code: "SALE10", percent: 10, minTotal: 1000,
});
expect(applied).toBe(false);
expect(cart.getTotal()).toBe(200); // İndirim yok
});
});
});En İyi Pratikler Özeti
// ✅ DO — Yap
// 1. Her test izole olmalı — beforeEach ile temiz state oluştur
// 2. Her test tek bir davranışı test etmeli — "ve" kelimesinden kaçın
// 3. AAA pattern kullan — Arrange, Act, Assert net ayrılsın
// 4. Test isimlerini açıklayıcı yaz — "ne, ne zaman, ne olur"
// 5. Edge case'leri test et — null, boş, sınır değerler, negatif
// 6. Hata senaryolarını test et — throw, reject, error state
// 7. Factory/Builder ile test verisi oluştur — okunabilir, bakımı kolay
// 8. Deterministic test yaz — zaman, random, ağı mock'la
// ❌ DON'T — Yapma
// 1. İmplementasyon detayını test etme — davranışı test et
// 2. %100 coverage hedefleme — %80-90 yeterli, kalite > sayı
// 3. Birden fazla şeyi tek testte test etme — ayır
// 4. Global state'e bağımlı test yazma — izole tut
// 5. Sleep/delay ile asenkron beklemek — await/findBy kullan
// 6. Koşullu mantık (if/else) test içinde kullanma — test basit olmalı
// 7. Testler arası veri paylaşma — her test kendi verisini oluşturmalı
// 8. Console.log ile "test etme" — assert kullan!
// 9. Gerçek API/DB'ye bağımlı unit test yazma — mock/fake kullan
// 10. Test kodunda DRY'ı aşırıya kaçırma — okunabilirlik > tekrar💡 İpucu: Test kodunda DAMP (Descriptive And Meaningful Phrases) prensibi, DRY (Don't Repeat Yourself) prensibinden daha önemlidir. Test kodunda biraz tekrar olması, okunabilirliği artırır. Helper fonksiyonlarla soyutlamayı aşırıya kaçırma — bir testi okurken ne olduğunu anlamak için 5 dosyaya bakmak zorunda kalmamalısın.
Özet
AAA Pattern (Arrange-Act-Assert): Her testin üç net bölümü olmalı — hazırla, uygula, doğrula. Bölümler arasında boş satır bırak.
Test Doubles: Dummy (yer tutucu), Stub (sabit cevap), Spy (izleme), Mock (cevap + doğrulama), Fake (basit implementasyon). Doğru türü seç.
Davranış vs İmplementasyon: Kodun ne yaptığını test et, nasıl yaptığını değil. Refactoring sonrası test kırılmamalı.
Coverage: %80-90 ideal hedef. Yüksek coverage ≠ iyi test. Kritik modüllerde %95+, utility'lerde %100, UI'da %70-80.
Flaky Test: Zaman, sıralama, race condition, random, ağ — her birinin çözümü mock ile izolasyon. Flaky test bulunca hemen düzelt.
Test Data: Factory ve Builder pattern ile okunabilir, bakımı kolay test verisi oluştur.
Her test izole, deterministic ve tek bir davranışa odaklı olmalı. DAMP > DRY.
AI Asistan
Sorularını yanıtlamaya hazır