Test-Driven Development (TDD)
Definition

TDD Example
// STEP 1: RED - Write a failing test
@Test
void shouldReturnZeroForEmptyCart() {
Cart cart = new Cart();
assertThat(cart.getTotal()).isEqualTo(Money.ZERO);
}
// Fails: Cart class doesn't exist
// STEP 2: GREEN - Write minimum code to pass
class Cart {
Money getTotal() {
return Money.ZERO;
}
}
// Test passes!
// STEP 3: RED - Write next failing test
@Test
void shouldCalculateTotalForSingleItem() {
Cart cart = new Cart();
cart.addItem(new Item("Widget", Money.of(10)));
assertThat(cart.getTotal()).isEqualTo(Money.of(10));
}
// Fails: addItem doesn't exist
// STEP 4: GREEN - Make it pass
class Cart {
private List<Item> items = new ArrayList<>();
void addItem(Item item) {
items.add(item);
}
Money getTotal() {
return items.stream()
.map(Item::getPrice)
.reduce(Money.ZERO, Money::add);
}
}
// Test passes!
// STEP 5: REFACTOR (if needed)
// Code is clean, no refactoring needed yet
// STEP 6: RED - Next test
@Test
void shouldApplyDiscountForOrderOver100() {
Cart cart = new Cart();
cart.addItem(new Item("Expensive", Money.of(150)));
assertThat(cart.getTotal()).isEqualTo(Money.of(135)); // 10% off
}
// Fails: No discount logic
// Continue the cycle...
Benefits & Trade-offs

TDD Patterns
// TRIANGULATION: Add tests to force generalization
// First test - could hard-code
@Test
void shouldMultiply2And3() {
assertThat(calculator.multiply(2, 3)).isEqualTo(6);
}
// Could pass with: return 6;
// Second test - forces real implementation
@Test
void shouldMultiply4And5() {
assertThat(calculator.multiply(4, 5)).isEqualTo(20);
}
// Now must implement: return a * b;
// FAKE IT TILL YOU MAKE IT
// Start with hardcoded values, generalize as tests demand
// First: return new ArrayList<>();
// Then: return items;
// Then: return items.stream().filter(...).collect(...);
// OBVIOUS IMPLEMENTATION
// If solution is obvious, just write it
@Test
void shouldAddNumbers() {
assertThat(calculator.add(2, 3)).isEqualTo(5);
}
// Obviously: return a + b;
// TRANSFORMATION PRIORITY PREMISE (TPP)
// Order of code transformations from simple to complex:
// 1. {} → nil (return nothing)
// 2. nil → constant (return literal)
// 3. constant → variable (return input)
// 4. statement → statements (add lines)
// 5. unconditional → conditional (add if)
// 6. scalar → collection (single → many)
// 7. statement → recursion (loop → recurse)
// 8. selection → iteration (if → while)
Outside-In vs Inside-Out

Common Pitfalls
// PITFALL 1: Testing implementation, not behavior
// BAD: Tied to implementation
@Test
void shouldCallRepositorySave() {
orderService.createOrder(order);
verify(mockRepository).save(any()); // Who cares?
}
// GOOD: Tests behavior
@Test
void shouldPersistOrder() {
orderService.createOrder(order);
assertThat(orderRepository.findById(order.getId())).isPresent();
}
// PITFALL 2: Writing too much test at once
// BAD: Giant test
@Test
void shouldCreateOrderCalculateTotalApplyDiscountAndSendEmail() {
// 50 lines of test...
}
// GOOD: Small, focused tests
@Test
void shouldCalculateTotal() { /* ... */ }
@Test
void shouldApplyDiscount() { /* ... */ }
@Test
void shouldSendConfirmationEmail() { /* ... */ }
// PITFALL 3: Not refactoring
// After green, don't skip refactor!
// Messy code + passing tests = technical debt
// PITFALL 4: Writing tests after code
// That's not TDD, that's "test-after"
// You lose design benefits
// PITFALL 5: Testing trivial code
// Don't write tests for getters/setters
// Don't write tests for constructors
// Focus on behavior with logic
Tips & Tricks
