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.
| Phase | What the developer does | Rule that applies |
|---|---|---|
| Red | Writes one test for the next small behavior | The test must fail, and for the expected reason |
| Green | Writes the minimal code that passes the test | No extra features, even obvious ones |
| Refactor | Removes duplication and improves names and structure | All tests stay green; behavior does not change |
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.
| Aspect | Classic (Detroit) TDD | Mockist (London) TDD |
|---|---|---|
| Direction | Inside-out, from core logic upward | Outside-in, from the user-facing behavior inward |
| Collaborators | Real objects where practical | Replaced by mocks and other test doubles |
| What tests verify | Resulting state and return values | Interactions and messages between objects |
| Main strength | Tests survive refactoring well | Interfaces are discovered early |
| Main risk | Slower feedback on integration design | Tests coupled to implementation details |
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 compared with related testing practices
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.
| Concept | What it is | Relation to TDD |
|---|---|---|
| Test-last development | Tests written after the code is finished | Same tests possible, but no design feedback |
| Unit test | Test of one small unit in isolation | The main vehicle of the TDD inner loop |
| Integration test | Test of components working together | Used sparingly; slower, so less frequent |
| End-to-end test | Test of a full user flow through the system | Can drive an outer ATDD loop |
| Test coverage | Share of code executed by tests | High coverage is a side effect, not the goal |
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.
| Situation | Recommended approach | Reason |
|---|---|---|
| Business rules, parsers, calculations | Strict TDD | Clear inputs and outputs, fast unit tests |
| Bug fix in existing code | Write a failing regression test first | Proves the bug and prevents its return |
| Legacy code without tests | Characterization tests, then TDD | Pins current behavior before changes |
| Exploring an unknown library or idea | Throwaway spike, then TDD | Learning comes before specification |
| Visual layout and styling | Visual review or snapshot tests | Correctness is hard to state as assertions |
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.