Skip to content

TDD: The Red-Green-Refactor Loop

The overview argued the case for working test-first: you write a small test that describes the next behaviour before the code that satisfies it exists. That framing raises an obvious mechanical question — how, exactly, do you do that without getting lost? You cannot write the whole test suite up front, and you cannot write all the code up front either. You need a tight, repeatable loop that tells you what to do next and when you are done with each step.

That loop has a name and a colour scheme: Red-Green-Refactor. It is the beating heart of Test-Driven Development, and it is small enough to fit in your head. Three phases, run over and over, each one lasting seconds to a couple of minutes. This page walks through every phase, explains why the order is non-negotiable, and drives the whole thing through a worked kata so you can see the rhythm rather than just read about it.

The loop is a cycle. You never “finish” it once; you spin through it once per tiny behaviour, dozens of times an hour.

┌─────────────────────────────────────────────┐
│ │
▼ │
┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ RED │───────►│ GREEN │───────►│ REFACTOR │─┘
└─────────┘ └─────────┘ └──────────┘
write a test simplest code clean up the
that FAILS that PASSES it code, stay green
(and confirm (fake it if you
it fails) have to)
  • RED — Write one small test that specifies the next behaviour you want, and run it. It must fail, and it must fail for the right reason (the behaviour is missing), not because of a typo or a compile error you didn’t intend. The bar is red. This is the step people are tempted to skip, and it is the most important one.
  • GREEN — Write the simplest code that makes that test — and every existing test — pass. Not the elegant version. Not the general version. The simplest thing that could possibly work, even if it is embarrassing. The bar goes green.
  • REFACTOR — Now, and only now, improve the code: remove duplication, rename things, extract a function, generalise the embarrassing hardcode. Run the tests after every small change. If the bar ever goes red during refactoring, you broke something — undo and try again. You refactor under a green bar, which is what makes it safe.

Then you go back to RED with the next behaviour. Tiny step, tiny step, tiny step.

The single most common mistake for newcomers is writing a test, seeing it pass immediately, and feeling good. That green is worthless. A test that has never failed proves nothing about your code — it only proves the test ran.

Consider what a passing-on-the-first-try test could mean:

  • The behaviour already existed (fine, but then this test taught you nothing new).
  • The test asserts something trivially true — assert_eq!(2, 2) dressed up in domain clothes.
  • The assertion is wired to the wrong variable, or the test never actually calls the code under test.
  • The test is in a file the runner silently skips, or a #[test] attribute is missing, so it was never executed at all.

Every one of those bugs is invisible if the test is green from birth. The red step is a test of the test. Watching it fail — and reading the failure message to confirm it fails because the behaviour is absent — is the only evidence you have that this test can ever catch a regression. A test you have never seen fail is a smoke detector you have never seen beep: you have no idea whether the battery is in.

test written ──► run it ──► does it FAIL?
┌──────────────────┴──────────────────┐
▼ ▼
YES, red NO, green
the test can detect the test is broken, or the
this behaviour's absence behaviour already exists —
→ proceed to GREEN investigate before trusting it

Nothing conveys the rhythm like watching it run. We’ll use a classic kata: a add function that takes a string of comma-separated numbers and returns their sum. The rules arrive one at a time, and we let a test drive each one. (The code is illustrative pseudo-Rust; the loop is identical in any language.)

RED. Start with the smallest possible behaviour. An empty string sums to zero.

#[test]
fn empty_string_is_zero() {
assert_eq!(add(""), 0);
}

Run it. It fails — add doesn’t exist yet, so it won’t even compile. That’s a legitimate red: the behaviour is absent. (In a compiled language, “doesn’t compile” is the failing state for the first test of a new function.)

GREEN. The simplest thing that could possibly work:

fn add(_numbers: &str) -> i64 {
0
}

Yes, we hardcoded 0. It is a lie that happens to pass. And that is fine — see the next section. The bar is green.

REFACTOR. Nothing to clean up yet. Move on.

RED. Now specify the next behaviour: one number returns itself.

#[test]
fn single_number_returns_itself() {
assert_eq!(add("7"), 7);
}

Run both tests. The new one fails (our function always returns 0), the old one still passes. Good — red for the right reason.

GREEN. Simplest code that passes both. We can no longer return a constant, so we do the least that works:

fn add(numbers: &str) -> i64 {
if numbers.is_empty() {
return 0;
}
numbers.parse().unwrap()
}

Both tests green.

RED.

#[test]
fn two_numbers_are_summed() {
assert_eq!(add("1,2"), 3);
}

Fails: "1,2".parse::<i64>() panics — parsing a comma-separated string as one integer is exactly the behaviour we haven’t written. Red.

GREEN. Now the hardcode is forced out into a real implementation:

fn add(numbers: &str) -> i64 {
if numbers.is_empty() {
return 0;
}
numbers
.split(',')
.map(|n| n.parse::<i64>().unwrap())
.sum()
}

Run all three tests. Green. Note something quietly powerful: the empty-string and single-number tests from earlier still pass — the split-and-sum version handles ""… actually, does it? "".split(',') yields one empty piece, which fails to parse. The is_empty guard is still doing real work. The old tests just proved that generalising did not break the earlier behaviour. That is the safety net working in real time.

REFACTOR. Three green tests give us cover to tidy. Maybe extract the parse, handle the panic-on-bad-input gracefully, or rename numbers to input. Change one small thing, rerun the suite, confirm green, repeat. The tests let you improve the code without holding your breath.

Each cycle above took under a minute. That is the whole point: the feedback is so fast that you are never more than one undo away from a working program.

”The simplest thing that could possibly work”

Section titled “”The simplest thing that could possibly work””

That phrase — Ward Cunningham’s — is the most misunderstood idea in TDD, so let’s be precise. In the GREEN step you are not trying to write good code. You are trying to get to green as fast as honestly possible, even by faking it.

Returning a hardcoded 0 in cycle 1 looks absurd, but it is a deliberate, disciplined move called faking it. Two things justify it:

  1. It keeps the step tiny. Your only job in GREEN is to satisfy the test in front of you. A hardcode is the minimum, so it is the least likely to introduce a second bug alongside the fix. Small steps mean small mistakes.
  2. The next test forces the honesty. A fake survives only as long as no test contradicts it. The moment you write cycle 2’s test, return 0 is exposed and must give way to real logic. This is a technique called triangulation — you add examples until a single constant can no longer satisfy them, and the general algorithm is the only thing left that fits. The tests drive out the fake.
one example ──► a constant passes it (fake it)
two examples ──► a constant can't fit both (triangulate)
→ the real algorithm is now the simplest thing that works

So “simplest thing that could possibly work” is not laziness — it is a discipline that stops you from writing speculative, untested generality. You never write code more general than the tests demand, which means every line of production code exists because a failing test asked for it. That is the whole guarantee TDD buys you.

Zoom out and the loop has a tempo. Good TDD feels like a metronome:

  • Steps are tiny. One test asserts one small behaviour. If a test needs a paragraph of setup or asserts five things, the step is too big — split it.
  • Cycles are fast. Seconds to a couple of minutes per loop. If you’ve been red for ten minutes, you bit off too much: revert to the last green, and take a smaller bite.
  • You always return to green. The green bar is home base. You leave it only briefly — for the length of one RED and one GREEN — and you come straight back. You never end a session, take a break, or commit while red. Green is the state you are allowed to stop in.

Under the hood — what “green” actually guarantees

Section titled “Under the hood — what “green” actually guarantees”

It is worth being honest about what the green bar does and doesn’t prove, so you don’t over-trust it. Green means: every example you have written down so far is satisfied. It does not mean the code is correct for inputs you never tested. The String Calculator above is green for "", "7", and "1,2" — and would happily return garbage for "1,,2", negative numbers, or a trailing comma, because no test has demanded otherwise yet.

That is not a flaw in TDD; it is the point. The green bar is exactly as strong as your examples. TDD’s job is to make sure code and examples grow together, one behaviour at a time, so that every behaviour the code claims is backed by a test that has been seen to fail without it. Widening the behaviour is the job of the next RED step — and of the property-based techniques later in this part, which attack the “inputs you never thought of” problem head-on.

  • Why does it exist? Red-Green-Refactor exists to make test-first development executable — to turn the vague instruction “write tests first” into a concrete, repeatable loop that tells you exactly what to do next and when each micro-step is done.
  • What problem does it solve? It attacks the gap between writing wrong code and discovering it. By keeping cycles to seconds and always returning to a known-good green bar, it makes mistakes tiny, cheap, and instantly localised to the last step.
  • What are the trade-offs? You write more test code, and up front it feels slower. The RED step is a real, non-optional cost. In exchange you get a regression net that grows with the code and a design pressure (covered on the next page) that tends toward smaller, more testable units.
  • When should I avoid it? Throwaway spikes and exploratory prototypes where you don’t yet know the behaviour you want — there is nothing to specify a test against. Also hard on code whose “correctness” is subjective (visual layout, tuning) where an example-based assertion can’t capture the goal.
  • What breaks if I remove it? Without the loop, tests drift after the code and become confirmation rather than specification. Without the RED step specifically, you lose the one proof that your tests can detect anything at all — and you inherit the green-suite-that-guards-nothing failure above.
  1. Name the three phases of the loop and state the one-line job of each.
  2. Why is a test that passes the first time you run it a warning sign rather than a success? What are you unable to conclude about such a test?
  3. In the String Calculator kata, returning a hardcoded 0 in cycle 1 passes the test. Why is that legitimate rather than cheating — and what mechanism eventually forces it to be replaced?
  4. You have been “red” for fifteen minutes, wrestling with a big change. What does the rhythm of TDD tell you to do, and why is that the right move?
  5. The suite is fully green. A colleague says “so the code is correct.” Give the precise, honest statement of what green actually guarantees.
Show answers
  1. RED — write one small test for the next behaviour and confirm it fails for the right reason. GREEN — write the simplest code that makes all tests pass. REFACTOR — clean up the code while the suite stays green, running tests after each small change.
  2. Because you have never seen it fail, so you have no evidence it can fail. It might assert something trivially true, test the wrong thing, never call the code under test, or not be run at all. Watching it fail is the only proof the test can catch a regression.
  3. It is the simplest thing that could possibly work — the minimum to reach green, which keeps the step tiny and unlikely to add a second bug. The next test (cycle 2, a single number) contradicts the constant; through triangulation, a hardcode can no longer satisfy multiple examples, so the real algorithm is forced out.
  4. Revert to the last green state and take a smaller bite. Fifteen minutes red means the step was too big; you lose only the current tiny step, and returning to green re-establishes a known-good base to attack the problem in smaller increments.
  5. Green guarantees only that every example currently written down is satisfied. It says nothing about inputs no test exercises. The code is correct exactly to the extent of your tests — no further.