SoftwareTestPilot
Topic 5 of 100

TDD (Test-Driven Development) — Red / Green / Refactor Explained

Test-Driven Development is a design discipline disguised as a testing practice. Writing the test first forces you to specify what 'done' looks like before you have the freedom to over-engineer.

Last updated: June 2026

Section 1

Executive Definition

Test-Driven Development (TDD) is a coding discipline in which a failing automated test is written before the production code that will make it pass. The practice was formalised by Kent Beck in the late 1990s as part of Extreme Programming and popularised through his 2003 book Test-Driven Development: By Example.

The heart of TDD is the red / green / refactor cycle. Red: write a small test that fails because the required behaviour does not yet exist. Green: write the simplest possible production code that makes the test pass. Refactor: improve the design of the passing code without changing its behaviour, using the test as a safety net. The cycle repeats in minute-scale increments, and each pass produces a slightly more capable system fully covered by regression tests.

TDD is often misread as 'a way to make sure you write tests'. That framing misses the primary benefit. The real payoff is design pressure: you cannot write a test for a class you have not conceptualised, so writing tests first forces you to make small, deliberate API decisions before implementation drags you into premature abstraction. The resulting code tends to be more decoupled, more testable, and closer to what the caller actually needs.

The economic argument is well documented. A 2008 IBM/Microsoft joint study measured 15–35% longer initial development on TDD teams and 40–90% fewer post-release defects. Later replications (Nagappan et al., Turhan et al.) found similar results with wider bands. The trade-off is real: TDD costs you time upfront in exchange for less bug-fix rework later. Whether the trade is worth it depends on how expensive your production defects are.

In 2026 the practice is normal in backend services and controversial in frontend UI code. The controversy is largely misplaced — the counterexamples people cite (visual polish, animation timing) were never good TDD candidates. Applied to pure logic — reducers, selectors, form validators, pricing engines — TDD is as productive on a React front-end as it is on a Spring backend.

Section 2

Architecture & Production Code

The TDD loop is small enough to fit on a sticky note but strict enough that most teams break it accidentally. Understanding the boundaries between phases is what separates real TDD from 'we wrote tests eventually'.

                      ┌─────────────────┐
                      │  Write a failing │
                      │  test (RED)      │
                      └────────┬─────────┘
                               │
                               ▼
                      ┌─────────────────┐
                      │  Simplest code   │
                      │  to pass (GREEN) │
                      └────────┬─────────┘
                               │
                               ▼
                      ┌─────────────────┐
                      │  Improve design  │
                      │  (REFACTOR)      │
                      └────────┬─────────┘
                               │
                               └──────┐
                                      │  next behaviour
                                      ▼
                              (loop again)

The RED phase is non-negotiable. A test written after the code already passes proves nothing — it might pass because of a bug in the assertion, and you would never know. Watching the test fail first is what verifies the assertion is real.

The GREEN phase forbids clever code. Write the ugliest, most literal implementation that satisfies the failing test. Cleverness belongs in the refactor phase, when the safety net is in place. Teams that skip this constraint end up over-engineering in the green phase and refactoring less confidently as a result.

The REFACTOR phase is the design payoff. With a passing test locking in behaviour, you can rename, extract, inline, and restructure with full confidence. Skipping refactor turns TDD into an over-fitting exercise — the tests pass but the code accumulates deuda técnica at the same rate as without TDD.

typescript
src/pricing/discount.ts (RED → GREEN → REFACTOR)
// STEP 1 — RED  (test written, no implementation exists yet)
import { describe, it, expect } from "vitest";
import { applyDiscount } from "./discount"; // ← does not exist

describe("applyDiscount", () => {
  it("returns full price when no coupons", () => {
    expect(applyDiscount(100, [])).toBe(100);
  });
});

// STEP 2 — GREEN  (simplest possible passing implementation)
export function applyDiscount(price: number, coupons: number[]): number {
  return price;
}

// STEP 3 — RED again (add the next behaviour)
it("subtracts a single coupon", () => {
  expect(applyDiscount(100, [0.3])).toBe(70);
});

// STEP 4 — GREEN
export function applyDiscount(price: number, coupons: number[]): number {
  if (coupons.length === 0) return price;
  return price * (1 - coupons[0]);
}

// STEP 5 — RED (stacking rule)
it("caps stacked discounts at 50%", () => {
  expect(applyDiscount(100, [0.3, 0.4])).toBe(50);
});

// STEP 6 — GREEN + REFACTOR
export function applyDiscount(price: number, coupons: number[]): number {
  const stacked = coupons.reduce((acc, c) => acc + c, 0);
  const capped = Math.min(stacked, 0.5);
  return Number((price * (1 - capped)).toFixed(2));
}
Section 3

TDD vs BDD vs ATDD

AspectTDDBDDATDD
Primary authorDeveloperProduct + Dev + QAProduct + QA
Test languagexUnit (code)Gherkin (business)Gherkin / DSL
Optimises forInternal designShared understandingAcceptance criteria
Test layerUnitAcceptance / integrationAcceptance
Cycle timeSeconds to minutesMinutes to hoursHours
Feedback audienceCompiler + developerWhole teamProduct + QA

In practice the three coexist. TDD runs continuously while a developer is coding. BDD and ATDD are ritual events tied to story kickoff. The winning combination is TDD at the unit level, BDD at the acceptance level, and exploratory testing on top.

Section 4

Production Debugging Scenarios

TDD failures rarely look like test failures — they look like design decisions that come back to hurt you weeks later. These three scenarios are the most common.

Scenario 1

Tests pass individually but fail in the full suite

Symptom
Vitest run in isolation is green; CI run of the whole file is red.
Root cause
Shared module-level state (a cached instance, a mocked module not restored).
Fix
Move state into a fixture and reset it in afterEach. Restore mocks with vi.restoreAllMocks() at the end of every test.
Scenario 2

Tests need constant updates after every refactor

Symptom
A rename of an internal method breaks 30 tests.
Root cause
Tests were written against implementation details, not behaviour.
Fix
Rewrite tests to assert on public outputs only. If a private method needs its own test, extract it into a real module.
Scenario 3

TDD skipped for 'obvious' code, defects appear in review

Symptom
Regex-heavy or date-boundary code lands untested and breaks in production.
Root cause
'Obvious' code was harder than it looked; the TDD ritual would have exposed the edge cases early.
Fix
Treat regex, dates, timezones, currency, and locale code as always-TDD. Add a lint rule that blocks merges of such modules without matching test files.

Practice this concept in a real QA interview

Run a live mock with our AI Interview Coach, tune your resume with the ATS Resume Reviewer, and screen live listings on the QA Jobs Radar.

People Also Ask

1.What does TDD stand for?
Test-Driven Development.
2.Who invented TDD?
Kent Beck rediscovered and formalised the practice in the late 1990s as part of Extreme Programming.
3.What is the red-green-refactor cycle?
Write a failing test, write the simplest code to pass it, then refactor with the test as a safety net. Repeat in minute-scale increments.
4.Does TDD slow you down?
Initially yes, by 15–35%. It typically pays back through fewer production defects and less rework.
5.Is TDD only for backend code?
No. It works on any code with clear inputs and outputs — reducers, validators, pricing engines — regardless of frontend or backend.
6.How is TDD different from BDD?
TDD is a developer-facing design discipline written in code. BDD is a whole-team practice written in business language.
7.Do I have to use xUnit for TDD?
No. Any test framework with a fast red/green feedback loop works — Vitest, pytest, JUnit, RSpec.
8.Can TDD replace code review?
No. Tests catch behavioural regressions; code review catches design and readability issues. Both are necessary.
9.Does TDD work with legacy code?
Yes, but you need seams first — Michael Feathers' 'Working Effectively with Legacy Code' documents the techniques. Start with characterisation tests.