Test-Driven Development (TDD) in Java
TDD flips the normal workflow: you write the test first, then write just enough code to make it pass. It sounds backwards, but it produces cleaner, more reliable code.
The Red-Green-Refactor Cycle
┌──────────┐ ┌──────────┐ ┌──────────┐
│ RED │────▶│ GREEN │────▶│ REFACTOR │
│ │ │ │ │ │
│ Write a │ │ Write │ │ Clean up │
│ failing │ │ minimal │ │ without │
│ test │ │ code to │ │ breaking │
│ │ │ pass │ │ tests │
└──────────┘ └──────────┘ └─────┬────┘
▲ │
└───────────────────────────────────┘
- RED — Write a test that fails (proves the feature doesn’t exist yet).
- GREEN — Write the simplest code that makes the test pass.
- REFACTOR — Improve the code without changing behavior. Tests stay green.
TDD Example: Shopping Cart
Step 1: RED — Empty cart has zero total
class ShoppingCartTest {
@Test
void emptyCartShouldHaveZeroTotal() {
ShoppingCart cart = new ShoppingCart();
assertEquals(BigDecimal.ZERO, cart.getTotal());
}
}
This won’t compile — ShoppingCart doesn’t exist yet. That’s expected.
Step 2: GREEN — Minimal implementation
public class ShoppingCart {
public BigDecimal getTotal() {
return BigDecimal.ZERO;
}
}
Test passes. ✅
Step 3: RED — Add one item
@Test
void shouldReturnPriceOfSingleItem() {
ShoppingCart cart = new ShoppingCart();
cart.addItem("Laptop", new BigDecimal("999.99"), 1);
assertEquals(new BigDecimal("999.99"), cart.getTotal());
}
Step 4: GREEN
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
public void addItem(String name, BigDecimal price, int quantity) {
items.add(new CartItem(name, price, quantity));
}
public BigDecimal getTotal() {
return items.stream()
.map(item -> item.price().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
record CartItem(String name, BigDecimal price, int quantity) {}
Step 5: RED — Multiple items
@Test
void shouldSumMultipleItems() {
ShoppingCart cart = new ShoppingCart();
cart.addItem("Mouse", new BigDecimal("25.00"), 2);
cart.addItem("Keyboard", new BigDecimal("75.00"), 1);
assertEquals(new BigDecimal("125.00"), cart.getTotal());
}
Already passes! ✅ (Our implementation handles it.)
Step 6: RED — Remove items
@Test
void shouldRemoveItem() {
ShoppingCart cart = new ShoppingCart();
cart.addItem("Mouse", new BigDecimal("25.00"), 1);
cart.addItem("Keyboard", new BigDecimal("75.00"), 1);
cart.removeItem("Mouse");
assertEquals(new BigDecimal("75.00"), cart.getTotal());
}
Step 7: GREEN
public void removeItem(String name) {
items.removeIf(item -> item.name().equals(name));
}
Step 8: REFACTOR
Look at the full class — is anything messy? Rename? Extract? The tests protect you.
TDD Rules (Uncle Bob)
- Don’t write production code unless it’s to make a failing test pass.
- Don’t write more of a test than is sufficient to fail (including compile errors).
- Don’t write more production code than is sufficient to pass the test.
Benefits of TDD
| Benefit | How |
|---|---|
| Better design | Forces small, testable units |
| Instant feedback | Know immediately if you broke something |
| Living documentation | Tests describe what the code does |
| Regression safety | Refactor with confidence |
| Less debugging | Bugs caught as you code |
When TDD Doesn’t Work Well
| Scenario | Why |
|---|---|
| Exploratory/prototype code | You don’t know the design yet |
| UI/visual code | Hard to test appearance |
| Infrastructure scripts | Better tested manually or with integration tests |
| Trivial getters/setters | Not worth testing |
| Third-party API wrappers | Test the integration, not the wrapper |
TDD vs Tests-After
| Aspect | TDD | Tests After |
|---|---|---|
| When tests are written | Before code | After code |
| Design influence | Tests shape design | Code shapes tests |
| Coverage | High (forced) | Often lower |
| Developer discipline | Required | Optional |
| Refactoring confidence | High | Medium |
| Speed (initially) | Slower | Faster |
| Speed (long-term) | Faster (fewer bugs) | Slower (more bugs) |
Practical Tips
- Start small — TDD a utility class first, not a complex service.
- One assertion concept per test — keep tests focused.
- Don’t skip REFACTOR — that’s where design improves.
- Name tests as specifications —
shouldThrowWhenEmailIsDuplicate. - Run tests frequently — after every small change.
Key Takeaways
- TDD = Red → Green → Refactor — test first, code second.
- Write the simplest code that passes the test — no more.
- Tests become living documentation of your system’s behavior.
- TDD forces good design — small methods, single responsibility, dependency injection.
- Use TDD where it adds value — skip it for trivial or exploratory code.
What’s Next?
Next: JaCoCo Code Coverage — measure how much of your code is tested and enforce minimum thresholds.