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?

# RED: no implementation exists def test_empty_string_is_balanced(): assert is_balanced("") == True # NameError - is_balanced doesn't exist # GREEN: the minimal code that passes THIS ONE test def is_balanced(s): return True
Verified directly — the minimal Green implementation was too minimal to prove anything
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

# GREEN (Cycle 2): a counter-based implementation - tracks depth only def is_balanced_v2(s): depth = 0 for ch in s: if ch in "([{": depth += 1 elif ch in ")]}": depth -= 1 return depth == 0 # RED (Cycle 3): a NEW test the previous cycles never considered def test_mismatched_types(): assert is_balanced("(]") == False # wrong bracket TYPE, not just unbalanced count
Verified directly — the counter-based implementation passed every prior test, then failed a genuinely new one
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.
# GREEN (Cycle 3 fix): a stack, tracking WHICH bracket, not just how many def is_balanced_v3(s): pairs = {')': '(', ']': '[', '}': '{'} stack = [] for ch in s: if ch in "([{": stack.append(ch) elif ch in ")]}": if not stack or stack.pop() != pairs[ch]: return False return len(stack) == 0
Verified directly — all 4 tests pass, including a further edge case added afterward
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.
Verified directly — the Refactor step changed the implementation with zero change to any test result
A cleaned-up version of 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

# exploratory: tuning a spam-score threshold against real, evolving data threshold_results = { 0.3: {'recall': 0.95, 'false_positive_rate': 0.40}, 0.5: {'recall': 0.80, 'false_positive_rate': 0.15}, 0.7: {'recall': 0.55, 'false_positive_rate': 0.04}, }
Verified directly — the algorithmic task needed zero test rewrites; the exploratory task needed 2
Every test written for the bracket checker — 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.
The deeper problem isn't the rewrites — it's that "correct" isn't even well-defined yet
The bracket checker has one right answer per input, knowable in advance from the rules of the problem. The threshold example doesn't — 0.7 has the lowest false-positive rate of the three, but 0.3 catches far more real spam (0.95 recall vs. 0.55); which one is "correct" depends on a business tradeoff no test assertion can encode until that tradeoff has actually been decided. Writing a test first here doesn't just cost rewrites — it forces a premature decision on a question the exploration itself is supposed to answer.
This is the same distinction Pseudocode & Algorithmic Problem-Solving already drew
That course's own Chapter 1 argued for designing an algorithm's shape before writing code, specifically for problems where the correct approach can be reasoned about in advance. TDD is that same discipline applied one level more granular — test-first fits naturally wherever the correct behavior is knowable before the code exists, and fits poorly wherever the whole point of the work is to discover what "correct" even means.

Where This Connects

This chapter's findingWhat it connects to
A depth-counter implementation missing bracket-type mismatches, caught by the next Red stepChapter 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 onePseudocode & 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

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

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