Test Driven Development

Red, Green, Refactor — A Field Guide to Test-Driven Development
RED — write a failing test GREEN — make it pass REFACTOR — clean it up
tests_first.md

Red, Green, Refactor
a field guide to Test-Driven Development

Why writing the test before the code changes what you build, not just when you build it — and how the three-step cycle keeps you honest.

8 MIN READ ENGINEERING PRACTICE

Most developers learn to write tests after the code works, as a way to prove it still works next week. Test-Driven Development inverts that order entirely: the test comes first, while the code it's testing doesn't exist yet. That single change in sequence is the whole discipline.

The practice is usually credited to Kent Beck, who described it as a way of resolving a paradox: "how do you know what you're going to implement before you know what you need?" The answer is that you don't try to know everything — you let one failing test define the very next small thing.

01The cycle

TDD runs on a three-beat loop, repeated for every piece of behavior you add. Each beat has one job, and skipping the order defeats the point.

STEP 1 Red write a test that fails
STEP 2 Green write the least code to pass
STEP 3 Refactor clean up, tests stay green

Here's the loop against a small example: a function that formats a shopping cart total.

cart.test.jsFAILING
// Step 1 — RED. The function doesn't exist yet.
test('formats a total with two decimals', () => {
  expect(formatTotal(19.5)).toBe('$19.50');
});

Running the suite now fails, correctly — there's nothing to call. That failure is not a bug, it's the spec.

cart.jsPASSING
// Step 2 — GREEN. Simplest thing that satisfies the test.
function formatTotal(amount) {
  return '$' + amount.toFixed(2);
}

No error handling, no currency support, no rounding strategy — none of that is asked for yet. Adding it now would be designing against imagination instead of a test. Once it's green, the refactor step is free to happen, because the test is the safety net.

cart.jsREFACTORED
// Step 3 — REFACTOR. Same behavior, clearer name and guard.
function formatCurrency(amount) {
  if (typeof amount !== 'number') throw new TypeError('amount must be a number');
  return '$' + amount.toFixed(2);
}

Then the loop repeats: the next test — maybe for negative amounts, or currencies other than dollars — starts red again.

02What it's actually buying you

The value of TDD isn't "more tests exist." It's that the order of operations forces a few good habits that are hard to sustain otherwise:

  • The test is a design tool, not a checkup. Writing the call site before the implementation forces you to think about the interface — arguments, return shape, naming — from the caller's point of view, before you've committed to an internal structure that's awkward to use.
  • Scope stays small on purpose. You can only write code that makes the current failing test pass. That constraint is what keeps a function from quietly growing five unrequested features.
  • Refactoring becomes safe instead of scary. A green suite is permission to change the internals aggressively, because anything you break announces itself immediately.
  • The suite doubles as documentation. A well-named set of tests reads as a list of the behaviors the system promises, in a form that can't silently go stale the way a comment can.
"Test-driven development is a way of managing fear during programming." — Kent Beck

03Where it goes wrong

The cycle is simple to describe and easy to do badly. A few failure modes show up often enough to be worth naming directly.

01

Testing implementation, not behavior

Asserting on private internals or exact call counts ties the test to how the code happens to work today. The refactor step then breaks tests for no behavioral reason, which trains people to stop trusting red.

02

Writing the test after the code anyway

Backfilling a test for code you already wrote checks that the code does what it does — not what it should do. The design pressure that makes TDD useful only exists when red comes first.

03

Skipping refactor because it's green

Green is a checkpoint, not a finish line. Code that passes but is left tangled compounds into a suite that's slow to extend, even though every individual test is technically fine.

04

Writing too big a test

A test covering five behaviors at once takes a long time to get to green, and a long red phase is where the discipline usually gets abandoned. Smaller steps keep the loop fast enough to actually repeat.

04Where it fits

TDD earns its keep fastest on logic with clear inputs and outputs — parsers, calculators, validation rules, state machines — where a test can pin down behavior precisely. It's a heavier lift around code whose main job is coordinating I/O, rendering, or third-party services, where the interesting bugs are often about integration rather than logic. Many teams apply it selectively: strict red-green-refactor for business logic, lighter integration or end-to-end tests around the edges.

The discipline is also easiest to learn on a small, self-contained function like the one above, and hardest to sustain under deadline pressure — which is exactly when skipping the red step feels tempting, and exactly when it costs the most.

red_green_refactor.md write the test you wish you had, then make it true
Share this post