Test-Driven Development: Red, Green & Refactor
TDD is a micro-cycle for discovering behavior and shaping interfaces: make one missing example fail, implement the smallest coherent behavior that passes, then improve structure while the behavior stays protected. It is useful where examples are predictable and fast tests compose into confidence, not as a universal coding ritual.
Test-driven development is not “having tests,” nor merely writing them before a large implementation. It is a short feedback cycle in which one concrete missing behavior drives one small change. The tests are both executable examples and a control system: they expose whether the next change moved behavior in the intended direction while leaving previously established behavior intact.
The cycle has three distinct jobs #
stateDiagram-v2 [*] --> Red: choose one missing behavior Red --> Green: implement enough behavior Green --> Refactor: all tests pass Refactor --> Red: choose the next behavior
Red: specify and observe absence #
Write the smallest example that expresses the next externally meaningful behavior. Run it and inspect the failure. Red is evidence that the test can detect the missing behavior; a test that starts green may be exercising an existing path, asserting nothing, or expressing the wrong condition.
The failure should be the expected one. A compile error can be a legitimate first red while introducing an API, but a missing fixture file or unrelated null dereference is not evidence about the behavior.
Green: make the example pass #
Implement the smallest coherent behavior that satisfies the example. This does not mean intentionally bad code or a hard-coded return that only fools the test. It means defer generality the examples do not yet require. If two implementations satisfy all current examples, choose the simpler one and let the next discriminating example force the distinction.
Refactor: change structure under protection #
With the suite green, remove duplication, improve names, move responsibilities and simplify interfaces. Refactoring changes structure while preserving behavior. Run the fast suite after each meaningful step. Skipping this phase turns TDD into test-first accumulation and leaves the design pressure unresolved.
Why test-first can influence design #
Writing the call site before the implementation forces an early answer to “How should a client express this intent?” Dependencies that are impossible to control, outputs that are impossible to observe, and objects that require enormous fixtures become visible before more code grows around them. This pressure often produces explicit inputs, cohesive operations and smaller public surfaces.
The pressure is informative, not automatically correct. Creating an interface solely so a mocking library can replace one concrete class may add ceremony without a real domain boundary. A test can also drive a distorted API if it is written from the implementation's perspective rather than a caller's. See interfaces and abstract types and test doubles.
Work in vertical behavioral slices #
Suppose an order accepts lines and computes a total. A useful sequence is:
- an empty order totals zero;
- one line contributes unit price times quantity;
- two lines sum independently;
- an invalid quantity is rejected;
- a promotion changes only eligible lines.
Each example adds an observable rule. A less useful sequence creates every class, repository and service interface first, then tries to make an end-to-end example pass. That is test-first scaffolding around an untested design, not a micro-cycle.
For an application feature, outside-in TDD may begin with an acceptance example and descend into focused unit examples as new responsibilities appear. Inside-out TDD grows core domain behavior first and adds adapters later. Neither direction removes the need for a real integration test at a protocol or database boundary.
TDD and regression testing solve different moments #
TDD guides code that does not yet exist. A regression test records a defect before fixing it. A characterization test records existing behavior before risky change. All produce executable checks, but their oracles come from different places:
| Practice | Oracle source | Primary purpose |
|---|---|---|
| TDD example | intended next behavior | guide implementation and interface design |
| regression test | reproduced defect and corrected expectation | prevent recurrence |
| characterization test | observed current behavior | detect unintended change in poorly understood code |
Confusing them creates false confidence. Copying current output into an expected value is not TDD unless that output was independently known to be the desired behavior.
Preconditions for a useful cycle #
TDD works best when:
- an expected result or invariant can be known before implementation;
- important cases can be predicted incrementally;
- tests run quickly and deterministically;
- the tested examples compose into confidence about the larger behavior;
- the implementation boundary can be observed without excessive scaffolding.
It is a poor fit for exploratory code whose purpose is to discover what output should exist, visual work judged primarily by a human, emergent performance under realistic load, or concurrent behavior that the test environment cannot control. TDD can still shape deterministic building blocks while separate experiments evaluate the emergent property.
Empirical studies do not support a universal promise that test-first order alone improves productivity or quality. Outcomes are entangled with task size, developer experience, cycle granularity, refactoring discipline and how much testing is performed. Treat TDD as a feedback technique with enabling conditions, not a badge.
Common failure modes #
- Large red: one test demands days of implementation, so failures carry little guidance.
- Green by special case: production code recognizes the fixture rather than implementing the rule.
- No refactor: duplication and awkward seams accumulate behind passing examples.
- Mock-driven structure: every internal call becomes an expectation and redesign becomes expensive.
- Always-green red step: the new test was never seen failing for the intended reason.
- Only happy examples: the cycle confirms a path but never sharpens boundaries or failure contracts.
- Slow suite: feedback arrives after the developer has built a large mental stack, defeating the mechanism.
The sustainable rhythm is small enough that the current failure, implementation change and design decision all fit in working memory. That is the actual advantage: not ritual purity, but a tight, inspectable loop between an example and the code that satisfies it.