Skip to content

TDD: red, green, refactor

One failing test at a time: make it run, then make it right.

Predict first

Before you read: predict

A pair test-drives resumeAt(position, duration) for a video app: the second at which an episode resumes. Before writing any code, they turn their whole list into tests:

Then they write the simplest code that passes the first test, and run all five:

export const resumeAt = (position) => position;

How many of the five tests fail?

Code for this question
test('resumes mid-episode', () => assert.equal(resumeAt(600, 3600), 600));test('restarts in the last 30 seconds', () => assert.equal(resumeAt(3590, 3600), 0));test('restarts from a negative position', () => assert.equal(resumeAt(-5, 3600), 0));test('starts a new episode at 0', () => assert.equal(resumeAt(0, 3600), 0));test('restarts at the end', () => assert.equal(resumeAt(3600, 3600), 0));
Pick one

The 30-second version

Test-driven development changes behaviour in small loops. List the cases first. Turn one case into a test and run it: it should fail, because the code cannot do that yet. Change the code, the simplest way, until that test and every earlier one pass. With everything green, tidy the design. Then take the next case, until the list is empty.

One failing test at a time: make it run, then make it right.

How it works

Step 1 of 11

A workflow for one change. Afterwards, what worked still works, the new behaviour works, and the code is ready for the next change.

Analogy: A swimming coach fixing a club swimmer's front crawl

Before the session she writes down the faults she wants gone.

At the pool In TDD
the faults on her clipboard before the swimmer gets in: a dropped elbow, a late breath, a scissor kick the list first (step 2)
one drill aimed at one fault, and a length to look at it: the elbow still drops one test, red (steps 3–4)
one cue, and only that cue, until the drill comes back clean green, the simplest way (step 6)
a full length after each fix, watching the faults fixed earlier as well every earlier test still passes (step 6)
a new fault seen mid-length goes on the clipboard, not into today's cue new cases join the list (step 7)
with every fix holding, she smooths the stroke's rhythm and watches another full length refactor on green (step 9)

Breaks down when: a fixed fault can creep back over weeks with nobody changing anything; code stays as it was until someone edits it, and its tests rerun in seconds, not lengths.

Contrasting case: a coach who reads out all ten corrections before the first length. The swimmer tries everything at once, the stroke falls apart, and nobody can tell which cue did it (step 11).

In real terms: say the next test, watch it fail, make it pass along with every earlier test, then tidy.

Examples, run for real

Each run calls every test written so far against the code as it stood (lib/run.mjs); crosscheck-node-test.mjs gets the same counts from node --test.

4.1 ExamHall EXH-4800: the floor in the wrong place

ExamHall's rule: a right answer scores 4, a wrong one costs 1, a blank scores 0; each section's score is floored at 0, and the exam adds the sections. The old code floored the exam's total once.

4.1 EXH-4800 old code, floor on the exam total · sections [right, right, right] and [wrong, wrong] · exam 10 · the rule gives 12

4.2 MR !5100: the pair kata, one test at a time

The pair rewrote the rule test-first in twenty minutes. They wrote the list, then took one case per cycle (steps 2–10).

4.2 list: empty section 0 · right +4 · wrong -1 · blank 0 · section floored at 0 · exam adds the sections
4.2 cycle 1 · new test: an empty section scores 0 · red 0 of 1 · code: return 0 · green 1 of 1
4.2 cycle 2 · new test: one right scores 4 · red 1 of 2 · code: 4 per right · green 2 of 2
4.2 cycle 3 · new test: a right and a wrong score 3 · red 2 of 3 · code: minus 1 per wrong · green 3 of 3 · refactor: one table of marks · green 3 of 3
4.2 cycle 4 · new test: a right and a blank score 4 · red 3 of 4 · code: a blank counts 0 · green 4 of 4
4.2 cycle 5 · new test: two wrongs score 0 · red 4 of 5 · code: floor the section at 0 · green 5 of 5
4.2 cycle 6 · new test: the exam adds floored sections (12 and 0 make 12) · red 5 of 6 · code: examScore sums sectionScore · green 6 of 6

Every new test failed first, and it was the only red test. Cycle 3's refactor moved the marks into one table with the tests green; that is why cycle 4's blank was red, until a blank counted 0. Cycle 6 pins EXH-4800: the floor sits on each section, so 12 and a floored 0 make 12.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 8

A chat app's unread badge is blank at 0 and shows the count otherwise. Its code and two tests pass:

test('no badge at 0', () => assert.equal(unreadBadge(0), ''));
test('shows the count', () => assert.equal(unreadBadge(7), '7'));

The last two cases on the pair's list are that a badge above 99 reads '99+' and that 99 itself still reads '99'. They turned both into tests, one at a time, and the log of those steps lost its order.

In what order did it happen, first to last?

Code for this question
export const unreadBadge = (count) => (count === 0 ? '' : String(count));

Your order

  1. 1empty
  2. 2empty
  3. 3empty
  4. 4empty
  5. 5empty
  6. 6empty
  7. 7empty

Tap the lines in the order you expect.

Question 2 of 8

A ticketing site refunds a cancelled booking in full 7 or more days before the show, half from 2 to 6 days before, and nothing after that. A pair has this code and two green tests:

test('full refund a week ahead', () => assert.equal(refundFor(80, 10), 80));
test('no refund the day before', () => assert.equal(refundFor(80, 1), 0));

Three cases are left on their list.

Which one, written as the next test, makes them change the code?

Code for this question
export function refundFor(price, daysBefore) {  if (daysBefore >= 7) return price;  return 0;}
Pick one

Question 3 of 8

A payments ledger's transfer fee has two green tests, and the pair has just added a third, which fails:

test('2% of a large transfer', () => assert.equal(fee(10000), 200));
test('no fee on nothing', () => assert.equal(fee(0), 0));
test('a 30-cent minimum', () => assert.equal(fee(500), 30));

Which new body for fee turns all three tests green?

Code for this question
export function fee(amountCents) {  return Math.round(amountCents * 0.02);}
Pick one

Question 4 of 8

A pair starts test-driving a video player's "Skip intro" button. Before any code exists, the navigator types the first test:

In typing it, which decision has the pair made?

Code for this question
test('skips to the end of the intro', () => assert.equal(skipIntroTo({ introEnd: 95 }, 12), 95));
Pick one

Question 5 of 8

the interviewer says: "ExamHall's multi-select questions need a scorer. Write multiSelectScore(chosen, correct): each chosen option that is correct earns 1, each chosen option that is not correct costs 1, a question never scores below 0, and an option chosen twice counts once. Don't change the arrays you're given. Let's pair: test-first." You have 20 minutes, a plain editor and node --test.

Before you type: say your test list out loud, each case with its input and what it expects.

Then say which test you will write first.

Question 6 of 8

the interviewer says: "ExamHall's multi-select questions need a scorer. Write multiSelectScore(chosen, correct): each chosen option that is correct earns 1, each chosen option that is not correct costs 1, a question never scores below 0, and an option chosen twice counts once. Don't change the arrays you're given. Let's pair: test-first." You have 20 minutes, a plain editor and node --test.

Now write it test-first in the time left: say the next test, see it fail, make it pass, tidy on green.

Question 7 of 8

A chat app's typing line passes these five tests:

With all five green, the pair refactors the function to this:

export function typingLabel(names) {
  const [first, ...rest] = names;
  if (!first) return '';
  if (rest.length === 0) return `${first} is typing`;
  if (rest.length === 1) return `${first} and ${rest[0]} are typing`;
  return `${first} and ${rest.length - 1} others are typing`;
}

They run the five tests.

How many fail?

Code for this question
test('nobody', () => assert.equal(typingLabel([]), ''));test('one person', () => assert.equal(typingLabel(['Ana']), 'Ana is typing'));test('two people', () => assert.equal(typingLabel(['Ana', 'Ben']), 'Ana and Ben are typing'));test('three people', () => assert.equal(typingLabel(['Ana', 'Ben', 'Cy']), 'Ana and 2 others are typing'));test('four people', () => assert.equal(typingLabel(['Ana', 'Ben', 'Cy', 'Di']), 'Ana and 3 others are typing'));
Pick one

Question 8 of 8

A video app makes one preview thumbnail per full 10 seconds of a clip, and never fewer than 1. The pair's first test passed on a hard-coded 6. Their second test, the two-minute clip, was red; while making it pass, the driver noticed that a clip shorter than 10 seconds would get 0, and wrote the minimum of 1 into the same change. A third case from their list followed, and all three tests pass:

test('a one-minute clip has 6 thumbnails', () => assert.equal(thumbnailCount(60), 6));
test('a two-minute clip has 12 thumbnails', () => assert.equal(thumbnailCount(120), 12));
test('a short clip has 3 thumbnails', () => assert.equal(thumbnailCount(30), 3));

A month later, a tidy-up turns the body back into Math.floor(seconds / 10).

Which tests fail?

Code for this question
export const thumbnailCount = (seconds) => Math.max(1, Math.floor(seconds / 10));
Pick one

Go deeper

Pro tips
  1. In a pair session, say the next failing test aloud before you write it; the interviewer hears the rhythm (a small test, a pass, a refactor) as you go. When: the row's typical question "Let's pair: implement this function test-first." (steps 3, 4, 6, 9). Verify:
    tip MR !5100 · 6 cycles · each new test red before its code: 6 of 6 · the only red test each time: the new one
    Status: RAN.
Source

quote-check.mjs finds every passage below verbatim in the saved text. Valid as of 2026-10-17.

  • Kent Beck, Canon TDD, Software Design: Tidy First?, 11 December 2023: https://tidyfirst.substack.com/p/canon-tdd (beck-canon-tdd.txt). The row's first source. Quoted: "Write a list of the test scenarios you want to cover" · "Turn exactly one item on the list into an actual, concrete, runnable test" · "Change the code to make the test (& all previous tests) pass (adding items to the list as you discover them)" · "Optionally refactor to improve the implementation design" · "Until the list is empty, go back to #2" · "Everything that used to work still works." · "The new behavior works as expected." · "The system is ready for the next change." · "The initial step in TDD, given a system & a desired change in behavior, is to list all the expected variants in the new behavior." · "Mistake: mixing in implementation design decisions." · "Mistake: convert all the items on the Test List into concrete tests, then make them pass one at a time." · "What happens when making the first test pass causes you to reconsider a decision that affects all those speculative tests? Rework." · "Now that you have a failing test, change the system so the test passes." · "Mistake: delete assertions so the test pretends to pass. Make it pass for real." · "If in the process of going red to green you discover the need for a new test, add it to the Test List." · "you need to decide whether to push on or start over (protip: start over but pick a different order to implement the tests)" · "Now you get to make implementation design decisions." · "Mistake: refactoring further than necessary for this session." · "Mistake: abstracting too soon. Duplication is a hint, not a command." · "Keep testing & coding until your fear for the behavior of the code has been transmuted into boredom." Supports steps 1–11.
  • Kent Beck, Test-Driven Development: By Example (2002): the row's second source is a book; it keys nothing. The headline's "make it run, then make it right" is quoted from the Canon TDD page ("Make it run, then make it right."), not the book.

Checked against the tools and docs as of 2026-10-17.