Testing: Unit, Integration, E2E
Bu bölümde hangi test türünü ne zaman kullanacağımızı ve testlerin sistem kalitesine neden doğrudan etki ettiğini ele alıyoruz. Amaç, en düşük maliyetle en yüksek güveni sağlayan bir test stratejisi kurmaktır.
Neden Test Yazarız?
Testler yalnızca “hata bulmak” için değildir; sürdürülebilir geliştirme için bir güvenlik ağıdır.
- Bir yeri düzeltirken başka bir yeri bozmayı engeller (regression)
- Yeni özellik eklediğinde bozulmayı erken yakalar
- Canlı ortamda hata yakalamak daha maliyetlidir
- Kodun ne yapması gerektiğini anlatan canlı bir dokümantasyon görevi görür
- Refactoring sürecini güvenli hale getirir
Test Piramidi
Test stratejisi genellikle “piramid” yaklaşımıyla kurulur:
- Unit: %60–70 (çok, hızlı)
- Integration: %20–30 (orta, daha yavaş)
- E2E: %5–10 (az, en yavaş)
Neden her şeyi E2E ile test etmiyoruz?
- E2E testler yavaştır
- kırılgandır (flaky)
- hata olduğunda kök sebebi net söylemeyebilir
- bakım maliyeti yüksektir
Bu yüzden ağırlık Unit testlerde olmalı, E2E ise yalnızca kritik akışları kapsamalıdır.
1) Unit Test
Unit test, sistemin en küçük işlevsel birimlerini (fonksiyon, method, class) izole şekilde test eder.
Ne Yapar?
- Business logic’i izole test eder
- Dış bağımlılıkları mock’lar (DB, dış API, cache)
- Milisaniyeler içinde çalışır
- Çok sayıda yazılabilir ve CI/CD için idealdir
Neden Unit Test Yazarız?
- Hataları erken yakalar
- Kodun güvenilirliğini artırır
- Değişikliklerin yan etkisini azaltır
- Refactoring’i kolaylaştırır
- Dokümantasyon niteliği taşır
AAA Döngüsü
- Arrange: Test verisini ve bağımlılıkları hazırla
- Act: Test edilen fonksiyonu çalıştır
- Assert: Beklenen sonucu doğrula
Mocking Nedir?
Mocking, dış bağımlılıkların gerçek implementasyonunu test ortamında kontrol edilebilir sahte nesnelerle değiştirmektir. Böylece test deterministik olur ve dış sistemlerden etkilenmez.
Unit test örneği:
describe('create-category', () => {
const mockCategory = {
id: 1,
name: "cat1",
description: "this is a category",
isActive: true,
slug: "cat.com"
};
beforeEach(() => {
jest.clearAllMocks();
});
test('Should create category', async () => {
CategoryModel.create.mockResolvedValue(mockCategory);
const result = await CategoryService.createCategory(mockCategory);
expect(result).toEqual(mockCategory);
expect(CategoryModel.create).toHaveBeenCalledTimes(1);
});
test('Should throw error if create category fails', async () => {
CategoryModel.create.mockRejectedValue(new Error('Db error'));
await expect(
CategoryService.createCategory(mockCategory)
).rejects.toThrow('Db error');
});
});
2) Integration Test
Integration test, birden fazla modülün birlikte doğru çalışıp çalışmadığını test eder.
Ne Yapar?
- API + DB gibi bileşenlerin birlikte çalışmasını doğrular
- gerçek query’ler çalışır
- config, migration, connection, transaction gibi sorunları yakalar
- unit testten daha yavaştır
Burada kullanılan DB production DB değildir; testler için ayrı bir test DB kullanılır (Docker/test container/in-memory DB gibi).
Örnek (Jest + Supertest):
it('should save user to database and return 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', password: '123' });
expect(response.status).toBe(201);
const user = await User.findOne({ where: { email: 'test@example.com' } });
expect(user).not.toBeNull();
expect(user.password).not.toBe('123'); // Hash kontrolü
});
Test Isolation
Integration testlerde testler birbirini etkilememelidir. Bu yüzden:
- her testten önce/sonra DB temizlenir (truncate)
- her test kendi verisini üretir
- testler bağımsız çalışır
3) E2E Test
E2E test, gerçek kullanıcı senaryosunu baştan sona doğrular. Frontend’den başlayıp backend, DB ve gerekiyorsa üçüncü parti servislerin uçtan uca çalışmasını kontrol eder.
Özellikleri
- En yavaş test türüdür
- UI değişikliklerinden kolay etkilenir
- bakım maliyeti yüksektir
- bu yüzden sadece kritik akışlar test edilir
Kritik akış örnekleri:
- kayıt olma ve login
- sepete ekleme ve ödeme
- şifre sıfırlama
Araç örneği: Cypress
Unit vs Integration Karşılaştırması
| Başlık | Unit Test (Mock) | Integration Test (Real DB) |
|---|---|---|
| Seviye | Fonksiyon / Class | Servis / Modül / API |
| Amaç | Kod doğru mu? | Parçalar birlikte doğru mu? |
| Bağımlılıklar | Mock/Stubs/Fakes | Gerçek bağımlılıklar |
| DB | Yok (mock) | Var (test DB) |
| Hız | Çok hızlı | Daha yavaş |
| Yakalanan Hatalar | Algoritmik / iş mantığı | Config, DB, migration, connection, transaction |
| CI/CD | Çok uygun | Seçili adımlarda |
Neyi Test Etmeyiz?
- Third-party kütüphanelerin kendi doğruluğu (ör. bcrypt çalışıyor mu)
- Framework/dil özellikleri (Express routing, Array.map)
- Mantık içermeyen boilerplate kod
- Private helper fonksiyonları tek tek test etmek
- Sadece getter/setter gibi trivial fonksiyonlar
- Sadece loglama/print işlemleri