100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Software Testing

TDD Case Study Walkthrough

A step-by-step worked example of building a small feature using the red-green-refactor TDD cycle.

PracticeIntermediate10 min readJul 10, 2026
Analogies

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.

python
# 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 price

Step 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

Was this page helpful?

Topics covered

#SoftwareTesting#TestingTDDStudyNotes#SoftwareEngineering#TDDCaseStudyWalkthrough#TDD#Case#Study#Walkthrough#Testing#StudyNotes#SkillVeris