Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Test-Driven Development

Pattern

A reusable solution you can apply to your work.

Also known as: TDD

“The act of writing a unit test is more an act of design than of verification.” — Robert C. Martin

Understand This First

Context

You’re about to implement a feature or fix a bug. You could write the code first and test it afterward, or you could flip the order and let the tests guide the design. This is a tactical pattern that changes how code gets written, not just how it gets checked.

Test-Driven Development builds on Tests, Harnesses, and Fixtures, but uses them as a design tool rather than just a verification tool.

Problem

When you write code first and tests later, the tests tend to confirm what the code already does rather than challenging whether it does the right thing. Tests written after the fact often miss edge cases, because the developer is already thinking in terms of the implementation they just wrote. Worse, “I’ll add tests later” often becomes “I never added tests.” How do you ensure that tests are thorough, that code meets its requirements, and that you write only the code you actually need?

Forces

  • Writing tests after code tends to produce tests that mirror the implementation rather than the requirements.
  • Without tests as a guide, it’s easy to over-engineer, building features nobody asked for.
  • Without tests as a safety net, refactoring is risky.
  • Writing tests first feels slow at the start of a task.
  • Some designs are hard to test, and discovering this late is expensive.

Solution

Write the test before you write the code. Kent Beck, who formalized TDD as part of Extreme Programming in the late 1990s, described the discipline this way: start by expressing a single, specific behavior you want the system to have, as a Test with a clear Test Oracle. Run the test and watch it fail. Then write the minimum code needed to make it pass. Once it passes, clean up the code through Refactoring. Repeat.

This approach has several effects. First, you never write code without a reason; every line exists to make a failing test pass. Second, you discover design problems early, because code that’s hard to test is usually code with too many dependencies or unclear responsibilities. Third, you accumulate a test suite as a side effect of development, not as a separate chore.

TDD doesn’t require writing all tests first. You write one test at a time, in small increments. The rhythm is what matters: test, code, clean up. The specific mechanics of this rhythm are described in Red/Green TDD.

How It Plays Out

A developer needs to build a function that validates email addresses. Before writing any validation logic, they write a test: assert is_valid_email("alice@example.com") == True. It fails because the function doesn’t exist yet. They create the function, returning True for any input. The test passes. They add another test: assert is_valid_email("not-an-email") == False. It fails. They add the minimum logic to distinguish valid from invalid. Step by step, the test suite and the implementation grow together, each informed by the other.

In agentic workflows, TDD becomes a powerful way to direct AI agents. Instead of describing what you want in prose, you write a failing test that defines what you want in code. Then you ask the agent to make the test pass. The agent has an unambiguous target, a green test, and can iterate autonomously until it gets there. This is often faster and more reliable than trying to describe the desired behavior in natural language.

Tip

When working with an AI agent, write the tests yourself and let the agent write the implementation. Your tests encode your intent; the agent’s code fulfills it. This division of labor plays to each party’s strengths.

Example Prompt

“I’ll write the tests, you write the implementation. Here’s the first test: assert is_valid_email(‘alice@example.com’) == True. Make it pass, then I’ll add the next test.”

Consequences

TDD produces code with high test coverage by construction. It tends to produce simpler designs, because you’re always writing the minimum code to pass the next test. The test suite becomes a living specification of the system’s behavior.

The cost is discipline and learning curve. TDD feels unnatural at first; writing a test for code that doesn’t exist yet requires thinking about behavior before implementation. It can also be misapplied: testing implementation details instead of behavior, or writing tests so fine-grained that they break with every refactoring. The goal is to test what the code does, not how it does it.

  • Depends on: Test, Test Oracle, Harness — TDD requires working test infrastructure.
  • Refined by: Red/Green TDD — the specific mechanical loop.
  • Enables: Refactor — TDD creates the safety net that makes refactoring safe.
  • Contrasts with: Regression — TDD prevents regressions; regression testing detects them after the fact.

Sources

  • Kent Beck formalized test-driven development as a named practice and described its mechanics in Test-Driven Development: By Example (2003). Beck has noted that he “rediscovered” rather than invented the technique — test-first programming appeared as early as D.D. McCracken’s 1957 programming manual and was used in NASA’s Project Mercury in the early 1960s.
  • TDD emerged from the Extreme Programming (XP) community in the late 1990s, where Beck and others applied the XP principle of taking effective practices to their logical extreme. The question “what if we wrote the tests before the code?” became a core XP discipline.
  • Robert C. Martin (quoted in the epigraph) championed TDD through his books Clean Code (2008) and The Clean Coder (2011), and codified the “Three Rules of TDD” that many practitioners follow today.
  • Martin Fowler’s Refactoring: Improving the Design of Existing Code (1999, 2nd ed. 2018) provided the vocabulary and catalog for the “refactor” step of the red-green-refactor cycle.