Test Yazma Disiplini
Değer üreten, yalancı yeşil görmeyen testler yazmayı öğreten skill — test piramidi, mock stratejisi, arrange-act-assert, flakiness önleme, hangi testi ne zaman yazacağının karar ağacı.
İçerik
Test Yazma Disiplini
Üç çeşit test kodu var: değer üreten, gürültü çıkaran ve güvence vermeden yeşil görünen. Bu skill üçüncü ikisine düşmeden birinciyi yazmayı öğretir.
Ne zaman aktif olur
- Yeni bir feature yazılırken (test-first veya beraber)
- Bug fix için regression testi eklerken
- Mevcut testi refactor ederken
- CI yavaş / flaky olduğunda kurtarma
Çekirdek prensipler
1. Test davranışı test eder, implementasyonu değil
Implementasyon testi: "getUserById fonksiyonu UserRepo.findOne çağırıyor mu?"
→ İç yapıyı değiştirirsen test kırılır, davranış değişmese bile. Kötü.
Davranış testi: "User ID'si ile aradığımda doğru kullanıcıyı alıyor muyum?" → İç değişse de test ayakta, davranış değişirse kırılır. İyi.
2. Test piramidi — ama mantıklı şekilde
/\
/E2E\ az (yavaş, flaky potansiyel)
/______\
/ INT \ orta (DB + gerçek servis)
/__________\
/ UNIT \ çok (hızlı, deterministic)
/______________\
Ama: Piramidin "kuralı" değil "yön tavsiyesi". Bazı takımlar trophy modelini (az unit, çok integration) tercih eder çünkü unit test çoğu zaman implementation-coupled oluyor. Uyarlama kritik.
3. Arrange - Act - Assert
Her test üç bölüm:
test("expired token reddedilmeli", () => {
// Arrange
const token = makeExpiredToken();
const authService = new AuthService(fakeClock);
// Act
const result = authService.verify(token);
// Assert
expect(result.ok).toBe(false);
expect(result.error).toBe("expired");
});
Bu sırayı bozma — Assert → Act → Arrange okunmaz.
4. Tek test, tek davranış
Tek test() içinde 5 farklı davranışı test etme. Bölerse okunur ve hangi case'in fail ettiği bir bakışta belli olur.
5. Mock gerçeği yalanlar
Her mock bir varsayım. Bu varsayım gerçekle uyumsuzsa test yeşil + prod kırmızı.
- İç kod için: mockla (DB, HTTP, clock, random)
- Kendi kütüphanen / bağımsız fonksiyon: mocklama, gerçeğini kullan
- Kritik yol boundary'leri: contract test ile mock'u gerçeğe eşitle
Hangi testi ne zaman yazarım
Unit test
Kullan:
- Saf hesaplama fonksiyonları (formatter, validator, calculator)
- Business rule / state machine
- Utility / pure logic
Kullanma:
- Her sınıf/dosya için "kaplama çıksın" diye
- 3 satır DI glue kodu için
- UI render detayı için (görünüm değişince test kırılmasın)
Integration test
Kullan:
- API endpoint (route → service → DB)
- Veritabanı ile iş kuralı
- Queue producer/consumer
- Auth/authorization flow
Kullanma:
- Her iç helper için (unit yeter)
E2E test
Kullan:
- Kritik yol: login → core action → logout
- Cross-page akış (signup → onboarding → first action)
- Browser-spesifik davranış (focus, keyboard nav)
Kullanma:
- Her buton click için (flaky + yavaş)
- UI'nın rengi / spacing'i için (visual regression ayrı araç)
Test adı formatı
Eski: test_1, should work, test login
İyi: Bir cümle — koşul + beklenti
test("süresi geçmiş token verify çağrısında 'expired' hatasıyla reddedilir")
test("valid idempotency key ile gönderilen tekrarlı istek cached response döner")
test("admin olmayan kullanıcı /admin/users erişiminde 403 alır")
İyi test adı = test kırıldığında ne olduğunu adından anlarsın.
Arrange — fixture yönetimi
Repeat yourself'ten kaçınma
Her test aynı createUser, seedOrders rutinini tekrarlıyorsa factory yaz:
// test/factories/user.ts
export function userFactory(overrides?: Partial<User>): User {
return {
id: crypto.randomUUID(),
email: "test@example.com",
role: "user",
createdAt: new Date("2026-01-01"),
...overrides,
};
}
// test
const admin = userFactory({ role: "admin" });
Fixed values
Random data yerine deterministik. Random kullanacaksan seed'li.
// ❌ flaky
const user = { id: Math.random(), name: faker.name.firstName() };
// ✅ stable
const user = userFactory({ id: "usr_test_001", name: "Alice" });
Act — doğru sınırı aramak
Test ettiğin birimi düzgün çiz. Küçükse implementation'a yaklaşır, büyükse setup patlar.
| Durum | Uygun sınır |
|---|---|
| Saf fonksiyon | Fonksiyonu çağır |
| HTTP endpoint | Supertest / fetch ile gerçek HTTP |
| React component | render() + user interaction (user-event) |
| Queue consumer | Mesajı queue'ya publish et, consumer'ın yazdığı DB'yi oku |
Assert — neyi iddia ediyorsun?
Gereksiz assertion'dan kaç:
// ❌ kırılgan
expect(user).toEqual({
id: "usr_001",
email: "a@b.com",
role: "user",
createdAt: expect.any(Date), // 3 gereksiz
updatedAt: expect.any(Date),
});
// ✅ odaklı
expect(user.email).toBe("a@b.com");
expect(user.role).toBe("user");
Ama eksik de etme:
// ❌ false positive riski
const result = await createOrder(...);
expect(result).toBeDefined(); // ne?
// ✅
expect(result.ok).toBe(true);
expect(result.value.status).toBe("pending");
Hata durumları test edilmez, test edilir
Mutlu yol yeterli değil. Her endpoint / fonksiyon için:
- Invalid input — 400 / error
- Not found — 404 / null
- Authorization fail — 403
- Conflict — 409 (duplicate, race)
- Upstream fail — 502/503 (mock ile simüle)
- Empty / zero / negative edge cases
- Unicode, emoji, very long strings
- Concurrent — race condition varsa
Flakiness — test yalan söylüyor
Nedenleri
-
Shared state: test A, test B'nin bıraktığı DB kaydını okur → Her test kendi fixture'ını oluştursun, cleanup etsin (transaction rollback)
-
Zaman:
new Date()test çalıştığı an farklı → Clock'u inject et (fakeClock) -
Random: UUID, faker seed'li olmadan → Seed ver veya deterministik değer
-
Network: gerçek external API çağrısı → Mock veya VCR / nock kaydı
-
Async race: setTimeout, setInterval olmadan test bitmiş ama event hala patlamadı →
await,waitFor, proper flush -
Order dependent: test A fail olunca B de fail → Test'ler bağımsız olmalı
Diagnose
Flaky test'i sil değil, izole et + debug et. 100 kere döndür:
for i in {1..100}; do pnpm test foo || break; done
İlk fail'de stack trace al, sebebi bul.
Coverage — hedef mi, yan ürün mü?
- %80+ kod coverage istatistikten anlamlı değildir (trivial getter dahil)
- %100 branch coverage kritik business rule'da anlamlı
- Kaplama % değil, kritik yolun kaplandığı önemli
Coverage tool'u guard, hedef değil.
Test ortamı
DB
- Memory DB (SQLite in-memory) — hızlı ama dialect farkı
- Real DB (docker-compose) — yavaş ama gerçeği simüle eder, tercih et
- Her test transaction içinde, sonunda rollback
Network
- Mock (msw, nock) — hızlı, kontrollü
- Recorded (VCR, Polly.js) — gerçek API kaydı, yavaş değişen için
- Real — dış SLA'ya bağımlı, CI flaky, kaçın
Somut örnekler
❌ Kötü test
test("user works", async () => {
const u = await createUser({ email: "a@b.com" });
expect(u).toBeTruthy();
const f = await findUser(u.id);
expect(f).toBeTruthy();
await deleteUser(u.id);
const g = await findUser(u.id);
expect(g).toBeFalsy();
});
Problemler: tek test 3 davranış, assertion belirsiz, cleanup sonda (fail durumunda orphan), isim hiçbir şey söylemiyor.
✅ İyi testler
describe("UserRepository", () => {
test("createUser, verilen e-posta ile yeni kullanıcı döner", async () => {
const user = await userRepo.create({ email: "a@b.com" });
expect(user.id).toBeDefined();
expect(user.email).toBe("a@b.com");
});
test("findById, var olan kullanıcıyı döner", async () => {
const created = await userRepo.create({ email: "a@b.com" });
const found = await userRepo.findById(created.id);
expect(found).not.toBeNull();
expect(found!.email).toBe("a@b.com");
});
test("findById, var olmayan ID için null döner", async () => {
const found = await userRepo.findById("non-existent");
expect(found).toBeNull();
});
test("delete, kullanıcıyı silip findById ile null döndürür", async () => {
const created = await userRepo.create({ email: "a@b.com" });
await userRepo.delete(created.id);
const found = await userRepo.findById(created.id);
expect(found).toBeNull();
});
});
Checklist — test commit'i öncesi
- Her
test()tek davranışı test ediyor - Test adları cümle — koşul + beklenti
- AAA sırası korundu
- Random / time / network mocklı (veya deterministik)
- Cleanup garantili (afterEach veya transaction rollback)
- Hata / sad path case'leri var
- Coverage % hedef değil, kritik yol kaplı
- Aynı test 50 kere arka arkaya çalıştırıldığında yeşil
Anti-pattern'ler
- ❌
expect(true).toBe(true)— placeholder test - ❌
test.skip(...)kalıcı olmuş — ya düzelt ya sil - ❌ "Kaplama çıksın" diye getter/setter testi
- ❌ Assert hiç yok — sadece çağırıyor, exception olmuyorsa yeşil
- ❌
try { await doSomething(); fail("should throw"); } catch(e) {}—expect().rejects.toThrow()kullan - ❌ Test dosyasında production logic (helper fonksiyonu production'dan ayrı yazma)
- ❌
sleep(1000)ile timing düzeltme — gerçek wait kullan (waitFor, polling) - ❌ Test içinde console.log kalıcı olarak
- ❌ Snapshot test her yerde — sadece karmaşık stable output için
- ❌ Shared mutable global state test'ler arası