---
title: Red-Green-Refactor
source: 'https://academia.sh/en/courses/unit-testing/red-green-refactor'
course: 'Unit Testing and Test-Driven Development'
language: en
updated: '2026-08-23T14:25:20+00:00'
license: 'CC BY-SA 4.0'
---

# Red-Green-Refactor

Running each step of the three-step cycle in which the test is written before the code, generalizing a green passed with a fixed value through triangulation, and the guarantee condition of the refactor step.

In the Unit Testing in Practice topic, the code came first in every test: a bug report, a
rule, or a refactor found an implementation already there to test. The test verified that
implementation, measured it, or exposed its fragility.

This topic reverses that order. When the test is written first, it stops being a
verification tool and becomes a **design tool**: the test decides which function will
exist, under what signature, what input it takes, and what it returns. This reversal is
called **test-driven development**, and it works through a three-step cycle.

The new rule to build is loan renewal. A member can extend the loan on a book they are
holding; a renewed loan's due day moves forward from the day of renewal by the loan
period for that member's type.

## Red: Testing Code That Does Not Exist Yet

The cycle begins with a failing test. For the test to run at all, the function under test
must **exist**; so the first thing written is an empty implementation, just enough to
compile.

```js
// renewal.mjs — version 0: function exists, no behavior
export function renew(record, today) {
  return record;
}
```

The test itself describes behavior that has not been written yet.

```js
// renewal.test.mjs — red step: single rule, no implementation yet
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { renew } from './renewal.mjs';

test('a student loan renewal moves the due day twenty-eight days past today', () => {
  const record = { memberNo: 'U-17', memberType: 'student', dueDay: 1028 };

  const updated = renew(record, 1020);

  assert.equal(updated.dueDay, 1048);
});
```

```sh
node --test --test-reporter=tap renewal.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - a student loan renewal moves the due day twenty-eight days past today
# tests 1
# pass 0
# fail 1
```

Three decisions were made in this step at once, and none of them while writing the
implementation. The function's name is `renew`, its inputs are a loan record and a day
number, its output is an updated record. This is the design side of test-driven
development: the interface is derived from the code that **uses** it.

The guarantee of the red run is the thing named in the first lesson: it was observed that
the test can fail. A test that is green the moment it is written may be testing the wrong
thing, or testing nothing at all; that possibility is ruled out only by watching a run
fail.

## Green: The Shortest Path

The second step has a single criterion: pass the test. Correct design, a general
solution, or clean code are not this step's goal; those belong to the third step.

```js
// renewal.mjs — version 1: shortest way to pass the test
export function renew(record, today) {
  return { ...record, dueDay: 1048 };
}
```

```sh
node --test --test-reporter=tap renewal.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - a student loan renewal moves the due day twenty-eight days past today
# tests 1
# pass 1
# fail 0
```

This version, which returns a fixed value, is obviously wrong, and deliberately so. What
it accomplishes is proving that the test really does check that value: the reason the run
turns from red to green is the line that was written, and nothing else.

This step is also a diagnostic tool. If the test still does not turn green even after the
fixed value is written, the problem is not in the implementation — it is in the test
itself.

## Triangulation

What exposes the fixed value's wrongness is not code — it is a **second example**. Writing
the same rule with a different input forces the implementation to generalize. This
technique is called **triangulation**.

```js
// renewal.test.mjs — version 2: second member type added
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { renew } from './renewal.mjs';

test('a student loan renewal moves the due day twenty-eight days past today', () => {
  const record = { memberNo: 'U-17', memberType: 'student', dueDay: 1028 };

  const updated = renew(record, 1020);

  assert.equal(updated.dueDay, 1048);
});

test('a standard member loan renewal moves the due day fourteen days past today', () => {
  const record = { memberNo: 'U-42', memberType: 'member', dueDay: 1014 };

  const updated = renew(record, 1020);

  assert.equal(updated.dueDay, 1034);
});
```

```sh
node --test --test-reporter=tap renewal.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - a student loan renewal moves the due day twenty-eight days past today
not ok 2 - a standard member loan renewal moves the due day fourteen days past today
# tests 2
# pass 1
# fail 1
```

The cycle is back to red, and this time what red points to is clear: the fixed value
is not enough. Generalizing is no longer a guess — it is a necessity imposed by the failing
test.

```js
// renewal.mjs — version 2: rule table instead of a fixed value
export const RULES = { student: { loanDays: 28 }, member: { loanDays: 14 } };

export function renew(record, today) {
  return { ...record, dueDay: today + RULES[record.memberType].loanDays };
}
```

```sh
node --test --test-reporter=tap renewal.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - a student loan renewal moves the due day twenty-eight days past today
ok 2 - a standard member loan renewal moves the due day fourteen days past today
# tests 2
# pass 2
# fail 0
```

## Refactor: Working on Top of Green

The third step has a precise definition: **refactoring** is improving internal structure
without changing observable behavior. No new rule is added, no new case is handled, no
bug is fixed. Only structure changes.

```js
// renewal.mjs — version 3: duration lookup moved into a named function
export const RULES = { student: { loanDays: 28 }, member: { loanDays: 14 } };

export function loanPeriod(memberType) {
  return RULES[memberType].loanDays;
}

export function renew(record, today) {
  return { ...record, dueDay: today + loanPeriod(record.memberType) };
}
```

```sh
node --test --test-reporter=tap renewal.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - a student loan renewal moves the due day twenty-eight days past today
ok 2 - a standard member loan renewal moves the due day fourteen days past today
# tests 2
# pass 2
# fail 0
```

What makes this step possible is the two steps before it. Without a green test suite,
refactoring is a rewrite, and its correctness can only be checked by eye. Every
structural change made while the tests are green is verified by the run itself.

The measurement from the Flaky Tests lesson finds its counterpart here. If the tests are
bound to the implementation, this step turns red and refactoring is punished. If the
tests are bound to behavior, the third step is free. Because test-driven development
derives its tests from the code that uses them, it makes writing behavior-bound tests
easier — but it does not guarantee it; the line between a contract and an internal detail
is still drawn by the person writing the test.

## The Questions the Three Steps Separate

The cycle's value is that its steps separate three questions that would otherwise have to
be considered all at once.

In the red step there is a single question: **what** do I want? What is the rule, how
will the interface be used, what is the expected result? Implementation is not considered
at all.

In the green step there is a single question: **how** do I make this test pass? The
shortest path is enough; design concerns are a distraction in this step.

In the refactor step there is a single question: **how does the code at hand get
better**? Whether the answer to that question is correct is reported by the tests.

Considering all three questions at once is a constant occurrence in an ordinary
programming session, and its cost is a cost of attention. The cycle lowers that cost by
splitting it into three.

## Summary

- In test-driven development, the test is not a verification tool but a design tool: the
  function's name, inputs, and output are derived from the test that uses it.
- The guarantee of the red step is seeing that the test can fail; a test that is green
  the moment it is written may be testing the wrong thing.
- The green step has a single criterion: pass the test; a version that returns a fixed
  value proves that the line just written is the reason for the pass.
- Triangulation forces the implementation to generalize with a second example;
  generalizing is not a guess but something the failing test demands.
- Refactoring improves internal structure without changing behavior, and it is only safe
  on top of a green test suite.

## Next Step

In this lesson the cycle turned twice, and each round handled a single rule. Step size is
a choice: the same work could have been carried out in a single round covering three
rules at once. That choice has a measurable cost — where a failing test points, how many
lines changed between two runs, and the distance back to the last green point. The next
lesson builds the same feature with two different step sizes and compares the two runs.
