TDD

On this page 7

TDD (test-driven development) is a software development practice in which a developer writes a small failing automated test first, then writes just enough production code to make it pass, and then refactors the code while keeping every test green. In programming and testing, TDD is less a testing technique than a design discipline: the tests specify behavior before it exists, so the code grows in short, verified steps.

The practice is usually summarized as the red-green-refactor cycle. Each loop typically lasts from under a minute to a few minutes, and a working session consists of dozens of such loops. The result is a suite of fast tests, mostly unit tests, that documents intended behavior and protects later changes.

How the red-green-refactor cycle works

The TDD cycle has three strict phases, and the discipline lies in not skipping or merging them. A developer first states one expected behavior as a test, watches it fail for the right reason, makes it pass by the simplest means, and only then improves the structure of the code.

The table below summarizes what happens in each phase and the rule that keeps it honest.

PhaseWhat the developer doesRule that applies
RedWrites one test for the next small behaviorThe test must fail, and for the expected reason
GreenWrites the minimal code that passes the testNo extra features, even obvious ones
RefactorRemoves duplication and improves names and structureAll tests stay green; behavior does not change
The three phases of a TDD cycle

Watching the test fail first is not a ritual. A test that passes before any code is written either tests nothing or tests the wrong thing, so the red phase verifies the test itself. The green phase deliberately accepts naive solutions, such as returning a constant, because the next test forces generalization. This technique is often called triangulation.

The refactor phase is where design emerges. With a safety net of passing tests, the developer can rename, extract functions and remove duplication without fear. Skipping refactoring is the most common way TDD degrades into merely writing tests early.

Schools and variants of TDD

TDD has two main schools that differ mainly in how they treat collaborators of the unit under test. They are known as the classic or Detroit school and the mockist or London school. Both follow the same cycle, but they produce different test suites and different design pressures.

AspectClassic (Detroit) TDDMockist (London) TDD
DirectionInside-out, from core logic upwardOutside-in, from the user-facing behavior inward
CollaboratorsReal objects where practicalReplaced by mocks and other test doubles
What tests verifyResulting state and return valuesInteractions and messages between objects
Main strengthTests survive refactoring wellInterfaces are discovered early
Main riskSlower feedback on integration designTests coupled to implementation details
Classic and mockist approaches compared

Related practices extend the idea beyond single units. Acceptance test-driven development (ATDD) starts each feature with an agreed acceptance test, and behavior-driven development (BDD) phrases such tests in a structured domain language such as Given-When-Then. Many teams combine an outer acceptance loop with an inner unit-level TDD loop.

TDD is defined by when tests are written and why, not by the type of test. It is therefore distinct from the test levels in the testing topic, although it relies heavily on them.

ConceptWhat it isRelation to TDD
Test-last developmentTests written after the code is finishedSame tests possible, but no design feedback
Unit testTest of one small unit in isolationThe main vehicle of the TDD inner loop
Integration testTest of components working togetherUsed sparingly; slower, so less frequent
End-to-end testTest of a full user flow through the systemCan drive an outer ATDD loop
Test coverageShare of code executed by testsHigh coverage is a side effect, not the goal
How TDD relates to neighboring concepts

A frequent misconception equates TDD with high test coverage. Code written test-first usually reaches high line coverage, often above 90 percent, but coverage measures execution, not correctness. Conversely, a team can reach the same coverage test-last and still miss the design feedback that comes from writing the test first.

Applying TDD effectively

TDD pays off most where behavior can be specified precisely and feedback is fast. Its value falls where requirements are exploratory or where tests are slow and expensive to run.

SituationRecommended approachReason
Business rules, parsers, calculationsStrict TDDClear inputs and outputs, fast unit tests
Bug fix in existing codeWrite a failing regression test firstProves the bug and prevents its return
Legacy code without testsCharacterization tests, then TDDPins current behavior before changes
Exploring an unknown library or ideaThrowaway spike, then TDDLearning comes before specification
Visual layout and stylingVisual review or snapshot testsCorrectness is hard to state as assertions
When TDD fits and when another approach works better

Practical habits make the cycle sustainable. Keep unit tests fast, ideally milliseconds each, so the whole suite runs on every change. Name tests after behavior rather than methods, write one logical assertion per test where possible, and keep a short list of pending test ideas so the current step stays small.

Limitations and common mistakes

TDD has real costs and failure modes. Controlled studies of its effect on defect rates and productivity report mixed results, and the benefit depends strongly on developer skill and the type of code. The typical problems are predictable and avoidable.

  • Testing implementation details produces brittle tests that break on every refactoring, which defeats the purpose of the safety net.
  • Overusing mocks can make tests pass while the real components fail to work together, so integration tests remain necessary.
  • Steps that are too large turn short loops into long debugging sessions; the fix is a smaller next test.
  • Skipping the refactor step leaves duplicated, poorly structured code covered by many tests.
  • Treating TDD as a complete test strategy ignores performance, security and usability testing.

TDD also has a learning curve. Teams new to the practice usually feel slower for several weeks, and the gains in confidence and maintainability appear later, when the codebase changes often.

The simple explanation

TDD means deciding how something should behave before building it, and writing that decision down as an automatic check. At first the check fails, because nothing has been built. The developer then builds just enough to make the check pass, tidies up, and moves on to the next small check.

It is a little like a builder who places a level and a measuring string before laying each row of bricks. The tools do not lay bricks, but they show immediately when a row is wrong. The key point is that the checks guide the work, not only inspect it afterward.

How it is used in practice

A developer at a logistics company must add shipping cost rules: parcels up to 2 kg cost a flat fee, heavier parcels add a charge per kilogram, and orders above a set value ship free. Using TDD, the developer first writes a test for a 1 kg parcel, sees it fail, and returns the flat fee. The next test, for a 5 kg parcel, forces the per-kilogram calculation.

Further tests cover the exact 2 kg boundary, the free-shipping threshold and invalid negative weights. After about fifteen short cycles, the module has a complete set of fast unit tests, each named after a business rule. Two months later, when the pricing changes, the team edits the rules, runs the suite in under a second, and ships the change with confidence because every existing rule is still verified.