Yazılım Projelerinde Test Driven Development (TDD) Nasıl Uygulanır?

Yunus Emre Güzel
23 Ocak 202515 dkFrontend
Yazılım Projelerinde Test Driven Development (TDD) Nasıl Uygulanır?

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:

  1. Red: Başarısız bir test yaz
  2. Green: Testi geçecek minimum kodu yaz
  3. 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

  1. Öğrenme Eğrisi

    • Pair programming ile başlayın
    • Küçük adımlarla ilerleyin
    • Kod kata'ları ile pratik yapın
  2. Test Maintenance

    • DRY prensibini testlerde de uygulayın
    • Test helper'ları ve fixture'ları kullanın
    • Test konfigürasyonunu merkezi yönetin
  3. Legacy Kod

    • Karakterizasyon testleri yazın
    • Kademeli refactoring yapın
    • Kritik yolları önceliklendirin

TDD Araçları ve Framework'leri

  1. Test Runner'lar

    • Jest
    • Vitest
    • Mocha
    • Jasmine
  2. Assertion Kütüphaneleri

    • Chai
    • Jest Matchers
    • Assert
  3. Mocking Araçları

    • Jest Mock Functions
    • Sinon
    • TestDouble
  4. 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:

  1. Küçük adımlarla başlayın
  2. Test coverage hedeflerini gerçekçi tutun
  3. Takım içi eğitim ve pair programming seansları düzenleyin
  4. Sürekli geri bildirim alın ve süreci iyileştirin

Kaynaklar