Test Driven Development TDD
Test-Driven Development (TDD)
TDD dreht die Entwicklung um: Erst Tests schreiben, dann Code. Lernen Sie den Red-Green-Refactor Zyklus und wie TDD zu besserem Code führt.
Der TDD-Zyklus
┌─────────────────────────────────────────────────────────────┐ │ TDD: RED-GREEN-REFACTOR │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ │ │ │ RED │ │ │ │ (Test │ │ │ │ fails) │ │ │ └────┬────┘ │ │ │ │ │ │ Write minimal │ │ │ code to pass │ │ ▼ │ │ ┌─────────┐ ┌─────────┐ │ │ │REFACTOR │◄──────│ GREEN │ │ │ │(Improve │ │ (Test │ │ │ │ code) │ │ passes) │ │ │ └────┬────┘ └─────────┘ │ │ │ │ │ │ Write next │ │ │ failing test │ │ └────────────────────────────────────────► │ │ │ │ 1. RED: Schreibe einen fehlschlagenden Test │ │ 2. GREEN: Schreibe minimalen Code zum Bestehen │ │ 3. REFACTOR: Verbessere Code ohne Funktionalität ändern │ │ 4. Wiederhole │ │ │ └─────────────────────────────────────────────────────────────┘
Beispiel: String Calculator (JavaScript)
// SCHRITT 1: RED - Erster fehlschlagender Test
// stringCalculator.test.js
describe('StringCalculator', () => {
it('should return 0 for empty string', () => {
expect(add('')).toBe(0);
});
});
// Test ausführen: FAIL ❌
// ReferenceError: add is not defined
// SCHRITT 2: GREEN - Minimaler Code zum Bestehen
// stringCalculator.js
function add(numbers) {
return 0;
}
module.exports = { add };
// Test ausführen: PASS ✅
// SCHRITT 3: RED - Nächster Test
describe('StringCalculator', () => {
it('should return 0 for empty string', () => {
expect(add('')).toBe(0);
});
it('should return the number for single number', () => {
expect(add('1')).toBe(1);
expect(add('5')).toBe(5);
});
});
// Test ausführen: FAIL ❌
// Expected: 1, Received: 0
// SCHRITT 4: GREEN - Code erweitern
function add(numbers) {
if (numbers === '') return 0;
return parseInt(numbers, 10);
}
// Test ausführen: PASS ✅
// SCHRITT 5: RED - Nächster Test
it('should return sum of two numbers', () => {
expect(add('1,2')).toBe(3);
expect(add('3,5')).toBe(8);
});
// Test ausführen: FAIL ❌
// SCHRITT 6: GREEN
function add(numbers) {
if (numbers === '') return 0;
const nums = numbers.split(',');
return nums.reduce((sum, n) => sum + parseInt(n, 10), 0);
}
// Test ausführen: PASS ✅
// SCHRITT 7: REFACTOR - Code verbessern
function add(numbers) {
if (!numbers) return 0;
return numbers
.split(',')
.map(Number)
.reduce((sum, n) => sum + n, 0);
}
// Tests erneut ausführen: PASS ✅
// Keine Funktionalität geändert, nur Code verbessert
Beispiel: User Service (PHP)
// tests/UserServiceTest.php
class UserServiceTest extends TestCase
{
// RED: Test für User-Erstellung
public function test_creates_user_with_valid_data(): void
{
$repository = $this->createMock(UserRepository::class);
$repository->expects($this->once())
->method('save')
->willReturn(new User(1, 'John', 'john@example.com'));
$service = new UserService($repository);
$user = $service->createUser('John', 'john@example.com');
$this->assertEquals('John', $user->getName());
$this->assertEquals('john@example.com', $user->getEmail());
}
}
// phpunit: FAIL ❌ - UserService existiert nicht
// src/UserService.php
// GREEN: Minimale Implementation
class UserService
{
public function __construct(private UserRepository $repository) {}
public function createUser(string $name, string $email): User
{
$user = new User(null, $name, $email);
return $this->repository->save($user);
}
}
// phpunit: PASS ✅
// RED: Test für Validierung
public function test_throws_exception_for_invalid_email(): void
{
$repository = $this->createMock(UserRepository::class);
$service = new UserService($repository);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid email format');
$service->createUser('John', 'invalid-email');
}
// phpunit: FAIL ❌
// GREEN: Validierung hinzufügen
class UserService
{
public function __construct(private UserRepository $repository) {}
public function createUser(string $name, string $email): User
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
$user = new User(null, $name, $email);
return $this->repository->save($user);
}
}
// phpunit: PASS ✅
// REFACTOR: Validierung extrahieren
class UserService
{
public function __construct(
private UserRepository $repository,
private UserValidator $validator
) {}
public function createUser(string $name, string $email): User
{
$this->validator->validate($name, $email);
$user = new User(null, $name, $email);
return $this->repository->save($user);
}
}
class UserValidator
{
public function validate(string $name, string $email): void
{
$this->validateName($name);
$this->validateEmail($email);
}
private function validateEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
}
private function validateName(string $name): void
{
if (strlen($name) < 2) {
throw new InvalidArgumentException('Name too short');
}
}
}
// Alle Tests erneut ausführen: PASS ✅
TDD Best Practices
1. TESTS ZUERST ❌ Code schreiben → Tests schreiben ✅ Test schreiben → Code schreiben → Refactor 2. KLEINE SCHRITTE ❌ Kompletten Feature-Test schreiben ✅ Kleinste testbare Einheit zuerst 3. NUR MINIMALER CODE ❌ Vorausschauend "nützlichen" Code schreiben ✅ Nur genau so viel Code, dass Test grün wird 4. EIN TEST NACH DEM ANDEREN ❌ Alle Tests auf einmal schreiben ✅ Red → Green → Refactor für jeden Test 5. TESTS MÜSSEN ERST FEHLSCHLAGEN ❌ Test schreiben, der sofort grün ist ✅ Sicherstellen, dass Test wirklich testet
Was TDD fördert
| Vorteil | Erklärung |
|---|---|
| Besseres Design | Code wird testbar geschrieben → Loose Coupling |
| Dokumentation | Tests dokumentieren erwartetes Verhalten |
| Confidence | Refactoring ist sicher dank Tests |
| Weniger Bugs | Bugs werden früh gefunden |
| Fokus | Nur nötiger Code wird geschrieben |
Test-Struktur: AAA Pattern
// Arrange - Act - Assert
describe('ShoppingCart', () => {
it('should calculate total with discount', () => {
// ARRANGE: Setup
const cart = new ShoppingCart();
cart.addItem({ name: 'Book', price: 20 });
cart.addItem({ name: 'Pen', price: 5 });
cart.applyDiscount(10); // 10%
// ACT: Ausführen
const total = cart.getTotal();
// ASSERT: Prüfen
expect(total).toBe(22.50); // 25 - 10% = 22.50
});
});
// Given - When - Then (BDD-Style)
describe('User Authentication', () => {
it('should lock account after 3 failed attempts', () => {
// GIVEN: Initial state
const auth = new AuthService();
const user = auth.createUser('test@example.com', 'password123');
// WHEN: Actions
auth.login('test@example.com', 'wrong1');
auth.login('test@example.com', 'wrong2');
auth.login('test@example.com', 'wrong3');
// THEN: Expected outcome
expect(user.isLocked()).toBe(true);
expect(() => auth.login('test@example.com', 'password123'))
.toThrow('Account is locked');
});
});
Mocking in TDD
// JavaScript mit Jest
describe('OrderService', () => {
it('should send confirmation email after order', async () => {
// Mock erstellen
const emailService = {
send: jest.fn().mockResolvedValue(true)
};
const orderRepository = {
save: jest.fn().mockResolvedValue({ id: 1 })
};
const orderService = new OrderService(orderRepository, emailService);
// Act
await orderService.placeOrder({
userId: 1,
items: [{ productId: 1, quantity: 2 }]
});
// Assert: Mock wurde aufgerufen
expect(emailService.send).toHaveBeenCalledTimes(1);
expect(emailService.send).toHaveBeenCalledWith(
expect.objectContaining({
type: 'order_confirmation',
orderId: 1
})
);
});
});
// PHP mit PHPUnit
class OrderServiceTest extends TestCase
{
public function test_sends_confirmation_email(): void
{
// Mock erstellen
$emailService = $this->createMock(EmailService::class);
$emailService->expects($this->once())
->method('send')
->with($this->callback(function ($email) {
return $email->getType() === 'order_confirmation';
}));
$repository = $this->createMock(OrderRepository::class);
$repository->method('save')
->willReturn(new Order(1));
$service = new OrderService($repository, $emailService);
// Act
$service->placeOrder(new OrderRequest(
userId: 1,
items: [new OrderItem(productId: 1, quantity: 2)]
));
// Assert erfolgt durch expects() oben
}
}
Wann TDD schwierig ist
TDD IST SCHWIERIGER BEI: 1. UI/Frontend Code → Stattdessen: Logik extrahieren, diese testen 2. Legacy Code ohne Tests → Erst charakterisierungs-Tests, dann Refactoring 3. Explorative Prototypen → Erst Prototyp, dann mit Tests stabilisieren 4. Externe Abhängigkeiten (APIs, DBs) → Mocking, Integration Tests separat 5. Zeitdruck / Deadlines → TDD zahlt sich langfristig aus LÖSUNG: TDD für Geschäftslogik, andere Strategien für Rest
💡 TDD Tipps:
1. Kleinste mögliche Schritte machen
2. Test muss erst ROT sein, dann GRÜN
3. Nur Code schreiben, der Tests bestehen lässt
4. Regelmäßig refactoren
5. Bei Schwierigkeiten: Noch kleinere Schritte
2. Test muss erst ROT sein, dann GRÜN
3. Nur Code schreiben, der Tests bestehen lässt
4. Regelmäßig refactoren
5. Bei Schwierigkeiten: Noch kleinere Schritte