Test-Driven Development: Red-Green-Refactor
Software Testing Strategy
Chapter 7 · Test-Driven Development: Red-Green-Refactor
Every prior chapter tested code that already existed. TDD reverses the order: write a failing test first, write only enough code to pass it, then clean up. This chapter walks the full cycle on a real algorithmic problem — verifying, along the way, a genuine gap that the incremental process catches — then gives TDD an honest limit: a task where the same discipline creates friction rather than value.
Red-Green-Refactor, Walked Through
Building a balanced-brackets checker: does "([{}])" have every bracket properly opened and closed, in the right order, of the right type?
is_balanced_v1's own "always return True" trivially passes both test_empty_string_is_balanced and a naive test_single_pair checking "()" — because it would pass literally any input. Only a test specifically checking an unbalanced case, is_balanced("(") == False, forces the implementation to do real work — it correctly fails against v1, as expected.
The Cycle That Caught a Real Gap
is_balanced_v2 correctly handled the empty string, a matched pair, and a single unclosed bracket — all 3 prior tests passed. Against the new test: is_balanced_v2("(]") returns True, but the correct answer is False — ( opens, ] closes, and a depth counter alone has no way to notice the bracket types don't match. This is a real, non-obvious gap that only surfaced because the next Red step deliberately asked a question the implementation had never been tested against.
is_balanced_v3 passes all 4 tests so far (empty, matched pair, unclosed, mismatched types), plus a Cycle 4 addition — is_balanced("([)"), brackets closed in the wrong order — also correctly returns False.
is_balanced_v3 (tidier variable use, an added tolerance for non-bracket characters) was checked against the full accumulated suite — 7 cases including two new ones ("([{}])" and "([)]") never run against the code before. All 7 passed. Refactoring changed the code's own shape; it changed none of its answers.
An Honest Limit: Where TDD Is a Weaker Fit
True for a matched pair, False for a mismatched type — was correct the first time it was written, because bracket balance has one objectively correct answer per input. Simulating the threshold-tuning exploration (trying 0.3 → 0.5 → 0.7, each new value chosen because it genuinely improved on the last by the metric being optimized): a TDD-first test asserting "the threshold IS 0.3" would have needed rewriting 2 times as better values were discovered through real experimentation, not derived in advance.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A depth-counter implementation missing bracket-type mismatches, caught by the next Red step | Chapter 3's own brittleness material — TDD's incremental tests are behavior-focused by construction, each one added because a new behavior needed proving |
| Zero test rewrites for a deterministic problem vs. 2 for an exploratory one | Pseudocode & Algorithmic Problem-Solving's own "design before you write" theme, applied at the level of an individual test rather than a whole algorithm |
Hands-On Exercises
Following this chapter's own Red-Green-Refactor cycle, add a new test for is_balanced("]") — a closing bracket with nothing open at all. Determine whether it passes or fails against is_balanced_v2 (the depth-counter version), and explain the result in terms of exactly what v2's own known gap is and isn't.
Continue this chapter's own threshold-exploration simulation with two more tried values, 0.55 and 0.65, added to threshold_results with plausible recall/false-positive-rate figures of your own choosing. Determine how many additional test rewrites (if any) this causes beyond the chapter's own count of 2.
Write one more Red-Green cycle for the bracket checker: a test for deeply nested, fully valid input, "((([[[{{{}}}]]])))". Verify it passes against the refactored implementation with no code changes at all, and explain what that confirms about the refactor's own correctness.
Chapter 7 Quick Reference
- Red-Green-Refactor: write a failing test, write the minimal code to pass it, then clean up while every test stays green
- Verified: a depth-counter implementation passed 3 tests, then failed a 4th ("(]" should be False) — a real gap the incremental process caught before shipping
- Verified: the refactor step changed the implementation's own shape with zero change to any of 7 accumulated test results
- Verified: an algorithmic task needed 0 test rewrites; an exploratory threshold-tuning task needed 2 — because only one of them has a knowable-in-advance correct answer
- The honest limit: TDD fits where correctness can be reasoned about before the code exists; it fights against exploratory work, where the code itself is how "correct" gets discovered
- Next chapter: Behavior-Driven Development — Given-When-Then, and living documentation that fails loudly instead of going stale silently