Ch. 22 · Software Testing

Red, Green, Refactor

Write a failing test first, make it pass minimally, then refactor under green, and understand why the order matters.

~2 min readintermediateupdated Oct 5, 2026

Test-driven development runs a short cycle: write a failing test (red), make it pass with the simplest change (green), then improve the design while the tests stay green (refactor). The order is the point, because each step answers a different question.

Before you start

You should be comfortable writing tests and small functions. This article covers the cycle and its discipline.

Step-by-step walkthrough

Step 1: Start red

Write a test for the behavior you want and run it to see it fail. A failing test proves the test can fail, which rules out a test that passes for the wrong reason. It also forces you to define the expected behavior before the implementation.

Step 2: Make it green minimally

Write the least code that passes the test, even if it is not elegant. The goal is a quick feedback loop and a known-good state. Over-engineering before the test passes hides which change made it work.

Step 3: Refactor under green

With the tests passing, improve names, remove duplication and adjust the design, re-running the tests after each step. A green suite means the refactor preserved behavior, which is exactly when it is safe to change structure.

Worked scenario

The cycle produces a tested function incrementally.

red:    test('empty cart total is 0', () => expect(total([])).toBe(0));  // fails
green:  function total(items) { return 0; }
red:    test('one item total is its price', () => expect(total([{price: 5}])).toBe(5)); // fails
green:  function total(items) { return items.reduce((s, i) => s + i.price, 0); }
refactor: extract and rename without breaking the green tests
Text

Walk through the example

Each red step defines the next behavior, and each green step satisfies it minimally, so the implementation grows only as tests demand. The refactor happens while everything passes, so a regression is caught immediately. The tests document the behavior as it was built.

Common mistake

Writing tests after the code to match what it does, which tests the implementation rather than the requirement and misses cases the code forgot. Another is skipping the refactor step, so the design stays rough even with tests.

Verify the behavior

Confirm each new test fails before the implementation exists. Check that making it pass required the smallest change. After refactoring, confirm the same tests still pass without edits, showing behavior was preserved.

Interview exercise

Why insist the test fails first?

Answer and reasoning

A test that has never failed might pass for the wrong reason, such as a mistaken assertion or a test that does not exercise the code. Seeing it fail proves the test is wired to the behavior and can detect a regression. It also forces you to specify the expected behavior before writing the implementation.

Continue learning

Compare structure in Arrange, act, assert and design in Test boundaries. Read the Martin Fowler on TDD and try the Testing interview questions.

More in Software Testing

read ✓Software Testing · hard

Testing Time with an Injected Clock

Inject a clock instead of calling the system time, freeze it in tests, and remove the sleeps that make tests flaky.

~2 min readread →
read ✓Software Testing · mid

Code Coverage and Its Limits

Read line and branch coverage as a signal, not a goal, and avoid the tests that chase the number without checking behavior.

~2 min readread →
read ✓Software Testing · hard

Scoping End-to-End Tests

Reserve end-to-end tests for critical user journeys, use stable selectors, and avoid duplicating coverage the lower tests already have.

~2 min readread →
esc