Test Driven Development (TDD), yazılım geliştirme sürecinde testlerin koddan önce yazıldığı bir metodoloji olarak karşımıza çıkıyor. Bu yaklaşım, daha güvenilir kod yazmanın yanı sıra, tasarım kararlarının erken aşamada alınmasını sağlıyor. Bu yazıda, TDD'nin temellerini, uygulama stratejilerini ve pratik örneklerini detaylıca inceleyeceğiz.
TDD Nedir ve Neden Önemlidir?
TDD, üç temel adımdan oluşan bir döngüye dayanır:
- Red: Başarısız bir test yaz
- Green: Testi geçecek minimum kodu yaz
- Refactor: Kodu temizle ve iyileştir
Bu yaklaşımın sağladığı avantajlar:
- Daha az hata içeren kod
- Daha iyi tasarım ve mimari
- Otomatik test coverage
- Güvenli refactoring
- Canlı dokümantasyon
- Hızlı feedback döngüsü
TDD Döngüsü: Pratik Bir Örnek
Basit bir hesap makinesi uygulaması üzerinden TDD döngüsünü inceleyelim:
// calculator.test.ts import { Calculator } from './calculator'; describe('Calculator', () => { let calculator: Calculator; beforeEach(() => { calculator = new Calculator(); }); // 1. RED: İlk başarısız test test('should add two numbers correctly', () => { expect(calculator.add(2, 3)).toBe(5); }); }); // calculator.ts export class Calculator { // 2. GREEN: Testi geçecek minimum kod add(a: number, b: number): number { return a + b; } } // 3. REFACTOR: Kodu iyileştir // Bu örnekte refactoring gerekmiyor, ama gerçek projelerde // kod kalitesini artırmak için refactoring yapılır
TDD ile Kompleks Bir Örnek
Daha karmaşık bir senaryo olarak, bir e-ticaret sepetini ele alalım:
// types.ts interface Product { id: string; name: string; price: number; } interface CartItem extends Product { quantity: number; } // cart.test.ts import { ShoppingCart } from './cart'; import { Product } from './types'; describe('ShoppingCart', () => { let cart: ShoppingCart; const sampleProduct: Product = { id: '1', name: 'Test Product', price: 100 }; beforeEach(() => { cart = new ShoppingCart(); }); // 1. RED: Ürün ekleme testi test('should add product to cart', () => { cart.addItem(sampleProduct); expect(cart.getItems()).toHaveLength(1); expect(cart.getItems()[0]).toEqual({ ...sampleProduct, quantity: 1 }); }); // 1. RED: Toplam fiyat testi test('should calculate total price correctly', () => { cart.addItem(sampleProduct); cart.addItem(sampleProduct); expect(cart.getTotalPrice()).toBe(200); }); // 1. RED: Ürün silme testi test('should remove product from cart', () => { cart.addItem(sampleProduct); cart.removeItem(sampleProduct.id); expect(cart.getItems()).toHaveLength(0); }); }); // cart.ts // 2. GREEN: Testleri geçecek implementasyon export class ShoppingCart { private items: CartItem[] = []; addItem(product: Product): void { const existingItem = this.items.find(item => item.id === product.id); if (existingItem) { existingItem.quantity += 1; } else { this.items.push({ ...product, quantity: 1 }); } } removeItem(productId: string): void { this.items = this.items.filter(item => item.id !== productId); } getItems(): CartItem[] { return [...this.items]; } getTotalPrice(): number { return this.items.reduce( (total, item) => total + (item.price * item.quantity), 0 ); } } // 3. REFACTOR: Kod iyileştirmeleri // - Tip güvenliği için interface'ler eklendi // - Immutability için spread operator kullanıldı // - Metodlar tek sorumluluk prensibine uygun
TDD Best Practices
1. Test Organizasyonu
// user.test.ts describe('User Authentication', () => { // Arrange: Test setup const testUser = { email: 'test@example.com', password: 'password123' }; describe('login', () => { // Happy path test('should login successfully with correct credentials', async () => { // Arrange const auth = new AuthService(); // Act const result = await auth.login(testUser); // Assert expect(result.success).toBe(true); expect(result.token).toBeDefined(); }); // Edge cases test('should fail with incorrect password', async () => { // Arrange const auth = new AuthService(); const wrongCredentials = { ...testUser, password: 'wrongpassword' }; // Act & Assert await expect(auth.login(wrongCredentials)) .rejects .toThrow('Invalid credentials'); }); }); });
2. Test İsimlendirme
// ❌ Kötü test isimlendirme test('login test', () => {}); // ✅ İyi test isimlendirme test('should return error when password is less than 8 characters', () => {}); test('should successfully create user when all inputs are valid', () => {});
3. Test Dublörleri (Test Doubles)
// payment.test.ts import { PaymentService } from './payment'; import { PaymentGateway } from './payment-gateway'; jest.mock('./payment-gateway'); describe('PaymentService', () => { let paymentService: PaymentService; let mockGateway: jest.Mocked<PaymentGateway>; beforeEach(() => { mockGateway = new PaymentGateway() as jest.Mocked<PaymentGateway>; paymentService = new PaymentService(mockGateway); }); test('should process payment successfully', async () => { // Arrange const paymentData = { amount: 100, currency: 'USD', cardToken: 'tok_123' }; mockGateway.processPayment.mockResolvedValue({ success: true, transactionId: 'tx_123' }); // Act const result = await paymentService.processPayment(paymentData); // Assert expect(result.success).toBe(true); expect(mockGateway.processPayment).toHaveBeenCalledWith(paymentData); }); });
4. Async Test Pattern
// api.test.ts describe('API Client', () => { test('should fetch user data successfully', async () => { // Arrange const api = new ApiClient(); const userId = '123'; // Act const user = await api.getUser(userId); // Assert expect(user).toEqual({ id: userId, name: expect.any(String), email: expect.any(String) }); }); test('should handle API errors gracefully', async () => { // Arrange const api = new ApiClient(); const invalidId = 'invalid'; // Act & Assert await expect(api.getUser(invalidId)) .rejects .toThrow('User not found'); }); });
TDD ile Domain-Driven Design (DDD)
TDD ve DDD'nin birlikte kullanımı, daha sağlam domain modelleri oluşturmanıza yardımcı olur:
// domain/order.test.ts describe('Order', () => { test('should calculate total with discounts', () => { // Arrange const order = new Order({ items: [ { productId: '1', quantity: 2, unitPrice: 100 }, { productId: '2', quantity: 1, unitPrice: 50 } ], discountCode: 'SAVE20' }); // Act const total = order.calculateTotal(); // Assert expect(total).toBe(200); // (2 * 100 + 1 * 50) * 0.8 }); test('should not allow negative quantities', () => { // Arrange & Act & Assert expect(() => { new Order({ items: [{ productId: '1', quantity: -1, unitPrice: 100 }] }); }).toThrow('Quantity must be positive'); }); });
Test Coverage ve Kalite Metrikleri
1. Jest Coverage Raporu
// package.json { "scripts": { "test": "jest --coverage" }, "jest": { "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } } }
2. Sürekli Entegrasyon (CI) Pipeline
# .github/workflows/test.yml name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: '18' - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Upload coverage uses: codecov/codecov-action@v2
TDD'nin Zorlukları ve Çözümleri
Öğrenme Eğrisi
- Pair programming ile başlayın
- Küçük adımlarla ilerleyin
- Kod kata'ları ile pratik yapın
Test Maintenance
- DRY prensibini testlerde de uygulayın
- Test helper'ları ve fixture'ları kullanın
- Test konfigürasyonunu merkezi yönetin
Legacy Kod
- Karakterizasyon testleri yazın
- Kademeli refactoring yapın
- Kritik yolları önceliklendirin
TDD Araçları ve Framework'leri
Test Runner'lar
- Jest
- Vitest
- Mocha
- Jasmine
Assertion Kütüphaneleri
- Chai
- Jest Matchers
- Assert
Mocking Araçları
- Jest Mock Functions
- Sinon
- TestDouble
E2E Test Araçları
- Cypress
- Playwright
- Selenium
Sonuç
TDD, yazılım geliştirme sürecinde kaliteyi ve güveni artıran önemli bir metodoloji olarak öne çıkıyor. Bu yaklaşımı benimseyerek:
- Daha güvenilir kod yazabilir
- Tasarım kararlarını erken alabilir
- Teknik borcu azaltabilir
- Maintenance maliyetlerini düşürebilirsiniz
TDD'yi projenize entegre ederken:
- Küçük adımlarla başlayın
- Test coverage hedeflerini gerçekçi tutun
- Takım içi eğitim ve pair programming seansları düzenleyin
- Sürekli geri bildirim alın ve süreci iyileştirin