TDD Case Study Walkthrough
This walkthrough builds a small 'discount calculator' feature entirely test-first, following the red-green-refactor cycle: write a failing test that expresses the next small piece of desired behavior (red), write the minimum code to make it pass (green), then clean up the implementation without changing behavior (refactor). Seeing the cycle applied to a concrete, evolving feature makes the abstract TDD rules — write only enough code to pass, never write code without a failing test first — concrete and checkable.
Cricket analogy: Building a feature test-first is like a batsman facing throwdowns before a match — you rehearse against a known ball (the failing test) before facing the unpredictable live bowling of production integration.
Step 1: Red — The Simplest Failing Test
We start with the smallest possible behavior: a cart with no discount code applies no discount. We write 'test_no_discount_code_returns_full_price' before the 'apply_discount' function even exists, so it fails with an ImportError or NameError. This forces the test to define the function's exact interface — its name, parameters, and return type — before any implementation exists, which is the core discipline of TDD: the test is a specification written first, not documentation written after.
Cricket analogy: Writing the first failing test is like a fresh net session where you set up the stumps and mark the crease before a single ball is bowled — the setup defines exactly what a valid delivery and dismissal will look like.
# Step 1: red — this test fails because apply_discount doesn't exist yet
def test_no_discount_code_returns_full_price():
result = apply_discount(price=100.0, code=None)
assert result == 100.0
# Step 2: green — minimum code to pass
def apply_discount(price, code):
return price
# Step 3: red again — new behavior, new failing test
def test_save10_code_applies_ten_percent_off():
result = apply_discount(price=100.0, code="SAVE10")
assert result == 90.0
# Step 4: green — extend minimally to pass both tests
def apply_discount(price, code):
if code == "SAVE10":
return price * 0.9
return priceStep 2: Green — Minimum Code, Not Full Design
After the first test fails, the TDD rule is to write the simplest code that makes it pass — even 'return price' with the discount code parameter unused counts, because no test yet requires branching logic. This feels uncomfortably naive to engineers used to designing up front, but it's deliberate: each subsequent failing test earns exactly the amount of additional logic needed, so the final implementation contains no untested code paths and no speculative generality nobody asked for.
Cricket analogy: Writing minimal code to pass one test is like a bowler practicing a single yorker line in the nets before adding variations — you don't build the full arsenal until each variation is individually demanded by a specific match situation.
It's normal for step 2 to look almost silly — hardcoding a return value. The discipline pays off two or three tests later, once the accumulated failing tests have forced genuinely necessary logic into existence, with zero code written 'just in case'.
Step 3: Refactor — Clean Up With a Safety Net
Once several discount codes exist as if/elif branches, refactor by extracting a discount-code-to-rate lookup dictionary, re-running the full test suite after each small change to confirm behavior is unchanged. The tests act as a safety net that lets refactoring be aggressive and confident rather than timid — because any behavioral regression is caught immediately, engineers can restructure code that would otherwise feel too risky to touch.
Cricket analogy: Refactoring with tests as a safety net is like a batsman tweaking their backlift technique in the nets with a bowling machine on — they can experiment aggressively because mistakes are caught immediately in a low-stakes setting, not during a live match.
Putting the Full Cycle Together
By the end of the case study, five red-green-refactor cycles have produced a discount calculator supporting percentage codes, flat-amount codes, and a minimum-purchase threshold rule, each added because a specific test demanded it, with a full regression suite proving every prior behavior still holds. This is the practical payoff of TDD: not 'writing tests' as an activity bolted onto development, but tests functioning as the design tool that drove every interface decision and the safety net that made every refactor cheap.
Cricket analogy: The end result — a feature shaped entirely by accumulated tests — is like a bowling action refined over years of net sessions, each tweak driven by a specific weakness a coach identified, resulting in an action that's provably sound rather than theoretically designed.
A common mistake when following case studies like this is writing several tests upfront and then implementing all the logic at once. That's test-after development wearing TDD's clothing — the discipline that actually matters is committing to writing exactly one failing test, watching it fail, then writing the minimum code, every single cycle.
- The red-green-refactor cycle: write a failing test, write minimum code to pass, then clean up with the tests as a safety net.
- The first failing test should fail because the function doesn't exist yet, forcing the interface to be defined by the test.
- Green-phase code should be the simplest thing that passes — no speculative logic beyond what the current test demands.
- Refactoring should only happen with a full green test suite, re-run after each small structural change.
- Each new behavior (a new discount type, a threshold rule) is added only because a new failing test demanded it.
- The end result is an interface and implementation entirely driven by accumulated tests, not upfront design guesses.
- Writing multiple tests before any implementation is test-after development, not real TDD.
Practice what you learned
1. In the red phase of TDD, why should the very first test typically fail due to a missing function or import error?
2. What is the correct approach during the green phase of TDD?
3. Why is refactoring considered safer under TDD than in code without tests?
4. What common mistake is described as 'test-after development wearing TDD's clothing'?
5. In the case study's final discount calculator, what determined which features (percentage codes, flat-amount codes, threshold rules) were implemented?
Was this page helpful?
You May Also Like
Choosing a Testing Framework
How to evaluate and select a testing framework based on language fit, assertion style, runner speed, and team workflow.
Testing Quick Reference
A condensed reference of core testing terminology, patterns, and rules of thumb for day-to-day use.
Testing Interview Questions
Common technical interview questions on testing and TDD, with the reasoning behind strong answers.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringDesign Patterns Study Notes
Software Design · 30 topics
Software EngineeringSoftware Engineering Study Notes
Python · 40 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics