HT
How Things Work
System Online

Test-Driven Development (TDD)

Writing the test before the code — the Red-Green-Refactor cycle, minimal implementations, emergent design, and when TDD genuinely helps versus when it gets in the way.

How It Works

Test-Driven Development is a discipline where you write a failing test first (RED), write the minimal code to pass it (GREEN), then refactor with the tests as a safety net (REFACTOR) — repeated in tiny cycles. It's as much a design practice as a testing one: tests specify behavior up front, code stays minimal, and a clean design emerges incrementally alongside a comprehensive, trustworthy suite.

1
RED — Test First

Before writing any production code, you write a small test for the next bit of behavior — and watch it fail. This proves the test actually tests something, and forces you to think about the desired API and outcome from the caller's perspective first.

2
GREEN — Make It Pass

Write the simplest possible code to make the failing test pass — even something naive. The goal is a green bar quickly, not elegance. This discipline stops you from writing speculative code that no test requires (YAGNI in action).

3
REFACTOR — Improve the Design

With all tests green, clean up: remove duplication, rename, restructure — confident that the tests will catch any regression. This is where good design emerges incrementally. The tests act as a safety net that makes refactoring fearless rather than risky.

4
Repeat in Tiny Steps

Loop the cycle for each new requirement, in small increments. The result is comprehensive tests written alongside the code (not bolted on later), a design shaped by real usage, and a suite you trust. TDD is a design discipline as much as a testing one.

Key Concepts

🔁Red-Green-Refactor

The TDD cycle: failing test → minimal passing code → clean up, repeated in tiny steps.

1️⃣Test First

Writing the test before the implementation, so it specifies behavior and is proven to fail meaningfully.

✂️Minimal Implementation

Writing only enough code to pass the current test — avoiding speculative, untested code (YAGNI).

🥅Safety Net

The growing green suite that lets you refactor aggressively without fear of breaking behavior.

🌱Emergent Design

Architecture that arises incrementally from real requirements and refactoring, not big upfront design.

👣Baby Steps

Small cycles keep feedback tight and the code always in a known-good (green) state.

Red → Green → Refactor
tsx
1// 1) RED — write a failing test that specifies the next behavior.
2test("fizzbuzz(3) is Fizz", () => {
3 expect(fizzbuzz(3)).toBe("Fizz"); // ❌ fails — not implemented yet
4});
5
6// 2) GREEN — the MINIMAL code to pass. Resist gold-plating.
7function fizzbuzz(n) {
8 if (n % 3 === 0) return "Fizz";
9 return String(n);
10} // ✅ all tests pass
11
12// 3) REFACTOR — improve design with the green suite as a safety net.
13function fizzbuzz(n) {
14 let s = "";
15 if (n % 3 === 0) s += "Fizz";
16 if (n % 5 === 0) s += "Buzz";
17 return s || String(n); // ✅ still green
18}
19
20// Repeat the tiny RED → GREEN → REFACTOR loop for each new requirement.
💡
Why This Matters

TDD is a well-known professional practice and frequent interview/discussion topic. Its real payoff — tests written with the code, designs shaped by usage, and the confidence to refactor — is valuable even if you don't follow it dogmatically. Knowing when it shines (logic-heavy code) and when it hinders (exploratory/UI work) is the mark of pragmatic judgment.

Common Pitfalls

⚠Dogmatic TDD everywhere — it fits logic-heavy code far better than exploratory spikes or UI layout work.
⚠Writing huge tests or skipping the 'see it fail' step, so you don't know the test actually tests anything.
⚠Never refactoring — stopping at GREEN leaves the duplication and mess TDD was meant to clean up.
⚠Testing implementation details, producing brittle tests that fight the refactor step.
⚠Treating TDD as merely 'writing tests' rather than a design feedback loop — missing most of its value.
Real-World Use Cases

1Untangling a Gnarly Algorithm

Scenario

A developer must implement a complex pricing/discount engine with many interacting rules and edge cases. Writing it all at once produces a tangle that's hard to verify and debug.

Problem

Complex logic written in one big push is error-prone and hard to test after the fact; bugs in edge cases hide until production, and there's no clear specification of intended behavior.

Solution

Use TDD: capture each rule as a small failing test, implement the minimum to pass, then refactor. The rules accumulate as executable specifications, edge cases are handled one at a time, and the design emerges cleanly. At the end there's a complete, trustworthy test suite.

💡

Takeaway: TDD excels for logic-heavy code with many rules and edge cases. Driving it test-by-test produces correct, well-specified, well-factored code and a comprehensive suite — instead of a big-bang implementation you can't fully trust.

2Refactoring Legacy Code Safely

Scenario

A team needs to refactor a critical but messy module. They're afraid to touch it because there are no tests and any change could silently break behavior.

Problem

Without tests, refactoring is gambling — you can't tell whether a change preserved behavior. Fear of breakage leads to code rot, since nobody dares improve the module.

Solution

Apply the TDD mindset in reverse: first add characterization tests that pin down current behavior (RED until they capture it, then GREEN), establishing a safety net. Then refactor in small steps, re-running the suite after each change to confirm behavior is unchanged.

💡

Takeaway: Tests are what make refactoring safe. Even on legacy code, building a test safety net first turns risky rewrites into confident, incremental improvements — the core value TDD bakes in from the start.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.