Test-driven development — red, green, refactor
TDD's actual claim isn't "write tests before code because tests are good" — it's that the specific order (a failing test, then just enough code to pass it, then cleanup) changes the design decisions you make along the way.
3 min read
The three-step cycle
// RED — write a test for behavior that doesn't exist yet; it fails, because nothing implements it
test("returns true for a valid email address", () => {
expect(isValidEmail("ada@example.com")).toBe(true);
});
// isValidEmail doesn't exist yet — this fails immediately, which is expected and correct
// GREEN — write the SIMPLEST code that makes the test pass, nothing more
function isValidEmail(str) {
return str.includes("@"); // deliberately minimal — just enough to pass THIS test
}
// REFACTOR — now that a passing test exists as a safety net, clean up freely
function isValidEmail(str) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str); // improved implementation, same passing test
}Red: write a test for behavior that doesn't exist, watch it fail — confirming the test actually tests something (a test that passes before the code exists is testing nothing). Green: write the minimum code needed to pass, resisting the urge to build out more than the current test demands. Refactor: with a passing test now acting as a safety net, improve the implementation's structure or clarity without changing its behavior — the test would immediately flag it if a "cleanup" accidentally changed what the code does.
The actual claim: the order changes the design, not just the paperwork
Writing the test first means designing the function's interface — what it's called, what it takes, what it returns — from the perspective of a caller, before any implementation exists to bias that thinking. It's easy to design an interface that leaks internal implementation details when the implementation is already written; writing the test first removes that temptation, since there's no implementation yet to leak from. This is the same effect the "tests as a design tool" idea from this domain's first lesson pointed at directly, just made systematic by writing the test before the code exists, every time, rather than only noticing it after the fact.
Where TDD's discipline pays off, concretely
TDD earns its keep most clearly on logic with real, non-trivial branching — parsing, validation rules, pricing calculations, state machines — where thinking through the test cases up front (what are the edge cases? what should happen for empty input, for the maximum value, for something malformed?) surfaces genuine questions about the requirements before a single line of the actual implementation commits to an answer. Each new failing test in this style also tends to represent one new requirement made concrete and executable, rather than a requirement someone remembers to write down separately and hope stays in sync with the code.
Where the strict cycle is a worse fit than the discipline it represents
For exploratory work — UI layout that's being visually iterated on, a spike to figure out whether an approach is even feasible at all, code whose desired shape isn't yet known — writing a precise test first can mean writing (and rewriting) tests for a design that's still actively changing, which is often more overhead than value. Many experienced practitioners use TDD's strict red-green-refactor cycle selectively, for the kind of logic described above, while writing tests after the code for exploratory work whose shape only becomes clear through building it — the same "test what matters" judgment this domain's coverage lesson already argued for, applied to when a test gets written rather than whether one does.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What are the three steps of the TDD cycle, in order?
2. What's TDD's actual claim, beyond just "write tests before code"?
3. Where does the strict TDD cycle tend to be a worse fit?