test-driven development

ruling

current authority

choose verification for the changed boundary. use test-first when it proves a material behavior; reversible, low-impact changes do not require new tests. do not delete existing implementation merely because an upstream procedure demands test-first.

the iron law: no production code without a failing test first. write the test, watch it fail for the right reason, write minimal code to pass, refactor while green.

what it teaches

red-green-refactor, each transition verified:

  1. RED — one minimal test, clear name, real code over mocks.
  2. Verify RED — run it; it must fail (not error) because the feature is missing. a test that passes immediately tests existing behavior; fix the test.
  3. GREEN — the simplest code that passes. no extra features, no "while I'm here" refactors.
  4. Verify GREEN — run it, and the rest of the suite.
  5. REFACTOR — clean up with tests staying green.

rationalizations ("too simple to test," "I'll test after," "tests after achieve the same goals," "TDD will slow me down") are each answered in the skill. tests written after code pass immediately, which proves nothing: you never watched them fail, so you never proved they can catch the bug.

why it exists

code written before its test is shaped by what the author already believes the code does. the test then verifies the cases the author remembered, not the ones they'd have discovered. test-first answers "what should this do?"; test-after answers "what does this already do?"

the rules

  • wrote code before the test? delete it — not keep as "reference," not "adapt" it. delete means delete.
  • bugs get a failing test reproducing them before the fix.
  • hard-to-test code is a design signal: hard to test equals hard to use.

one full cycle:

// RED: one minimal test, real code over mocks
test("an expired approval cannot be accepted", async () => {
const approval = expiredApproval();
const result = await acceptApproval(approval);
expect(result).toEqual({ ok: false, error: "approval_expired" });
});
// verify RED: this run must FAIL because acceptApproval does not exist yet.
// GREEN: the smallest implementation that passes.
type Result = { ok: true } | { ok: false; error: "approval_expired" };
async function acceptApproval(approval: Approval): Promise<Result> {
if (approval.expiresAt <= Date.now()) return { ok: false, error: "approval_expired" };
return { ok: true };
}
// verify GREEN: run the test, then the whole suite. refactor only while green.

when to use

any feature, bugfix, refactor, or behavior change. exceptions (throwaway prototypes, generated code, config) require the human partner's explicit permission.

good and bad

bad

implementation: write the approval service
test: add assertions that match the implementation

the test confirms remembered behavior rather than defining the contract first. a test written after the code passes immediately, so it proves nothing about its ability to fail: the author writes assertions around what already happens and never discovers the edge cases the contract should have forced. when the bug this sequence missed arrives in production, the suite reports green and the investigation starts from zero.

good

test: an expired approval cannot be accepted
result: failing
implementation: smallest change that makes the contract pass

the failing behavior establishes the boundary before implementation narrows it. watching the test fail for the right reason proves the harness can detect the missing behavior, so a later pass is evidence the code changed something real. kept up across a codebase, this compounds into a suite whose green actually means the contract holds, and into code shaped by its intended use rather than its author's assumptions.

search pages

go to any page