Test-Driven Development (TDD)

Test-Driven Development is a workflow where you write a failing test first, then write just enough code to pass it, then clean up — using the tests to drive the design of your code. Done well, it produces better-factored interfaces, fewer regressions, and a suite of executable specifications that document what the code is supposed to do.

TDD is a tool, not a religion. It shines for logic you can express as clear inputs and outputs, and it's awkward for exploratory or UI-heavy work. The value isn't the tests alone — it's the design pressure that writing tests first applies to your code.

TL;DR

Quick Example

The cycle in miniature — write the failing test before the function exists:

The Red–Green–Refactor Cycle

  1. Red — write a failing test for one tiny behavior. This confirms the test actually tests something (a test that's never red proves nothing).
  2. Green — write the simplest code to pass. Resist premature generalization.
  3. Refactor — clean up structure, naming, and duplication while keeping tests green.

Repeat in small steps. Each loop should be quick.

What TDD Is Really Optimizing

Writing the test first forces you to use your code before you build it, which pushes you to:

It also turns requirements into executable specifications that stay in sync with the code.

Practical Guidelines

Test structure conventions

When TDD Fits (and When It's Harder)

Great fit: domain logic, pure functions, validation and parsing, business rules, and bug fixes (write a test that reproduces the bug first).

Harder: UI-heavy code without good testing seams, deeply async/distributed workflows, and exploratory prototypes. In those cases, lean on higher-level integration or E2E tests rather than forcing unit-level TDD.

Best Practices

Common Mistakes

Brittle tests that mirror implementation

Over-mocking

FAQ

Do I have to write tests before code to "do TDD"?

Yes — that's the defining feature. The failing test first is what creates the design pressure and guarantees the test can actually fail. Writing tests after code is still valuable (it's just testing), but it doesn't drive design the same way and tends to validate whatever you already built rather than question it.

Does TDD slow development down?

It front-loads effort and can feel slower per feature, but it usually pays back through fewer bugs, less debugging, safer refactoring, and living documentation. The net effect depends on context — it's a clear win for complex domain logic and long-lived code, less so for throwaway prototypes. Use it where the payoff is real.

How is TDD different from BDD?

TDD is a developer workflow centered on the red-green-refactor cycle and unit-level design. BDD (Behavior-Driven Development) frames tests as human-readable behavior specs (Given-When-Then), often spanning higher levels and involving non-developers. They're compatible — BDD is essentially TDD with a vocabulary and structure aimed at describing behavior and collaborating on requirements.

What do I do when code is too hard to test?

Treat the difficulty as a design signal. Hard-to-test code usually has hidden dependencies, does too much, or mixes concerns. Refactor toward smaller units with injected dependencies and clear inputs/outputs. The same changes that make code testable (decoupling, single responsibility) also make it better-designed — which is much of TDD's point.

Related Topics

References