The Code That Knew What It Wanted
The Code That Knew What It Wanted
I did not always write tests first. I thought it was a waste of time. Why write tests for code that does not exist yet? Write the implementation, then verify it works.
That logic is backward. Writing the test first is what forced me to think about the design before committing to it.
Three Simple Rules#
TDD is not about testing. It is about design. The rules are simple and non-negotiable:
- You are not allowed to write any production code unless it is to make a failing test pass.
- You are not allowed to write any more of a test than is sufficient to fail.
- You are not allowed to write any more production code than is sufficient to pass the failing test.
It sounds rigid. That rigidity is where the value lies.
A Story in Red, Green, and Refactor#
A simple inventory system. Not a full ERP. Just enough to track items and quantities.
Cycle 1: Add Item to Inventory#
@Test
void addsNewItemToInventory() {
Inventory inventory = new Inventory();
inventory.addItem("keyboard", 10);
assertEquals(10, inventory.getQuantity("keyboard"));
}
This does not compile. Create the Inventory class:
public class Inventory {
public void addItem(String name, int quantity) {
}
public int getQuantity(String name) {
return 0;
}
}
The test runs and fails. Implement just enough to pass:
public class Inventory {
private Map<String, Integer> items = new HashMap<>();
public void addItem(String name, int quantity) {
items.put(name, quantity);
}
public int getQuantity(String name) {
return items.getOrDefault(name, 0);
}
}
Green. Nothing to refactor. Move on.
Cycle 2: Add Quantity to an Existing Item#
@Test
void addsQuantityToExistingItem() {
Inventory inventory = new Inventory();
inventory.addItem("mouse", 5);
inventory.addItem("mouse", 3);
assertEquals(8, inventory.getQuantity("mouse"));
}
Red. Fix the implementation:
public void addItem(String name, int quantity) {
int existing = items.getOrDefault(name, 0);
items.put(name, existing + quantity);
}
Green.
Cycle 3: Remove Items#
@Test
void removesQuantityFromItem() {
Inventory inventory = new Inventory();
inventory.addItem("monitor", 10);
inventory.removeItem("monitor", 4);
assertEquals(6, inventory.getQuantity("monitor"));
}
Red. Implement:
public void removeItem(String name, int quantity) {
int existing = items.getOrDefault(name, 0);
items.put(name, existing - quantity);
}
Green. But consider what happens if the quantity goes negative.
Cycle 4: Prevent Negative Quantity#
@Test
void doesNotAllowNegativeQuantity() {
Inventory inventory = new Inventory();
inventory.addItem("cable", 2);
inventory.removeItem("cable", 5);
assertEquals(0, inventory.getQuantity("cable"));
}
Fails. Fix it:
public void removeItem(String name, int quantity) {
int existing = items.getOrDefault(name, 0);
int newQuantity = Math.max(0, existing - quantity);
items.put(name, newQuantity);
}
Green.
Notice what happened: the test revealed a behavior requirement that the implementation had not considered. The test asked the question. The implementation answered it.
Why It Matters#
TDD is not about being careful. It is about being fast. You write less code, not more. You stop guessing at requirements and start responding to explicit behavior specifications. You stop being afraid to refactor because the test suite confirms nothing broke.
The tests tell you what to do next. They guard your back while you move forward.
The Discipline#
TDD is not always easy. Sometimes you want to break the rules—just this one time, just a quick fix, no test.
Do not.
The moment you stop writing tests first, you stop doing TDD. You are coding without a specification, and the code will reflect that.
Refactoring without tests is like modifying a running system without the ability to verify it still works. You find out what broke only when something fails in production.
When Tests Come First, Bugs Come Last#
With TDD, you do not just produce code. You produce verified code. You know it works because you watched it grow, test by test, each new behavior confirmed before the next one was introduced.
The tests do not just catch bugs. They define what the code is supposed to do. That definition persists long after the original author has moved on.
One Last Thing#
Do not take this on faith. Pick a feature. Write the test first. Watch it fail. Make it pass. Refactor. Repeat.
After a week, you will not want to write production code without a test waiting to be satisfied.