Skip to content

Test-Driven Development

Test-Driven Development (TDD) writes a failing test before the code that makes it pass. The tests are the visible output, but the real product is design pressure: code that is hard to test is hard to use, and TDD surfaces that before the code has callers.

The methodologies section covers TDD as an XP practice. This page looks at it as a testing approach.

The cycle

graph LR
    R[Red: write a failing test] --> G[Green: simplest code that passes]
    G --> F[Refactor: remove duplication, improve names]
    F --> R
Step Rule Why
Red One small test, watched failing A test never seen failing may be testing nothing
Green The least code that passes, even if ugly Keeps the step small enough that a failure points at the last edit
Refactor Improve design with tests green The design work lives here, not in Green

Minutes per loop. An hour-long cycle means the step was too big.

Inside-out and outside-in

Two schools, both TDD, differing in where they start.

graph TD
    CLASSIC["Classicist: start at the domain core"] --> C1[Real objects, few doubles]
    CLASSIC --> C2[Assertions on state]
    CLASSIC --> C3[Design emerges from refactoring]
    LONDON["London school: start at the entry point"] --> L1[Drive out collaborators as interfaces]
    LONDON --> L2[Assertions on interactions, with mocks]
    LONDON --> L3[Design decided by the conversation between objects]
Classicist London school
Doubles Few, mostly at real boundaries Many, one per collaborator
Verification State Interaction
Refactoring cost Low Higher, tests know the call structure
Good for Rich domain logic Coordination-heavy, layered code

Most teams do well starting classicist and reaching for interaction verification only where the interaction genuinely is the requirement.

What it produces beyond tests

  • Testable design. Dependencies get injected because otherwise the test cannot be written, which yields loosely coupled code as a side effect.
  • Coverage that means something. Every line exists because a test demanded it, so there is no untested code to backfill later.
  • Executable specification. The test states intent in a form that fails when the code drifts from it.
  • Small steps. Debugging time drops because the change since the last green state is minutes old.

Where it is misapplied

Symptom Cause
Tests break on every refactor Testing implementation instead of behaviour, usually over-mocking
Well-tested mess The refactor step is being skipped
Tests written after the code, never seen red May be permanently green by accident
Six mocks per test The unit has too many dependencies, and TDD is reporting a design problem

TDD also fits poorly where the correct output is unknown in advance, such as exploratory data work or spikes. Spike first, throw the spike away, then drive the real implementation with tests.

Check Your Understanding

#

Why must the test be observed failing before the production code is written?

#

A team writes tests first, keeps them green, and never refactors. What results?