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.