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
