Case file P1

Test-First Development

The test is the brief. Write it first.

The test is the brief. Write it before the code, watch it fail for the right reason, then make it pass with the smallest change you can. Beck called it red, green, refactor. The broader frame is that the test is the artefact you commit to first because it is the artefact that proves the behaviour ever existed.

ByAdam LewisPublished3 May 2026Revised13 Oct 2026Reading12 minVersionv1.2ConfidenceHigh
§0b

Opinion

I have lost the “tests first or tests after” argument exactly once and I lost it because I gave up. Every other time the “I'll add the tests later” commit has ended the same way: the tests never appear, the regression surfaces a quarter later and the person who would have written them has moved teams. Beck's 2003 book put a name on the cycle and changed who I hired;1 twenty-three years later, the case is stronger, not weaker, because the same argument now binds the agent in the loop as well as the human.

I want to plant a flag on this: the test is the brief rather than a check on the brief. Pair it with T1 Domain-Driven Types and the function signature plus the failing assertion together encode what design notes used to. The common counter, “we don't do TDD because the design isn't clear yet”, names exactly the moment when the discipline pays most. A test that cannot be written cheaply is the design saying the unit is doing too much, which is F1 Single Responsibility arriving in test-shaped clothes.

Agents made this cheaper. Karpathy argues that anything resettable, efficient, and rewardable can be optimised by a model;4 the failing test is the cheapest reward signal a developer ever produced. With tests written first, the agent loop has a target to hit. Without them, the agent generates plausible code at machine speed and the gap surfaces only when something explodes in production. P1 is the floor under AI4 Verifiable Specs: the AC is the brief at the work scale, the test is the brief at the code scale, and both must be machine-checkable.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

The test is the brief. Write the failing test before the implementation, run it, watch it fail for the reason you predicted, then write the smallest code that makes it green. In typed languages, the type is the first test: define the signature, let the compiler reject the obvious failures, then write the test for the residue.

/tenet/test-first-development/P1
Copy for paste
§0c

AI eyes only

Rule: the test is the brief. Write it first. Run it failing. Then implement.

Reject: writing implementation before a failing test exists. Reject: writing the test after the implementation. Reject: relying on introspection as the design signal.

Generate: write a test that names the requirement, run it, paste the failure into the plan, then implement until the test passes. Each new behaviour earns a new failing test first.

Diagnostic: the test must fail before the implementation exists. If it passes vacuously, the test is wrong. Rewrite it before continuing.

§0d

Why?

  • The test is the working specification, readable, runnable, falsifiable. The function signature plus the failing test together carry what the prose docstring used to.
  • Every shipped behaviour leaves a regression test behind it. The bug that reaches production once does not go out again silently: the suite catches the second occurrence before the diff is merged.
  • Writing the test first produces a callable design before any implementation choices have been sunk. A unit you cannot test cheaply is a unit doing too much and the friction shows up before the cost is locked in.
  • In typed languages the type is the first test, see T1 Domain-Driven Types. A signature that compiles has already ruled out one whole class of failure.
  • Agents iterate on the signal you give them. A failing test is a signal an agent can read, re-run, and grade itself against. A description in chat is not. P1 puts AI4 Verifiable Specs to work at the function scale.
  • Tests-first PRs review faster. The reviewer reads the test to learn the intent, reads the code to verify the test, and is done. Tests-after PRs ask the reviewer to reverse-engineer the intent and check the test is honest in the same pass.
  • A green suite is a refactoring licence. Without tests, every cleanup is a leap of faith; with them, the cleanups in P6 Leave it Better stop being leaps and become small, safe steps.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

The discipline has a date and an author. Kent Beck's Test-Driven Development by Example (Addison-Wesley, 2003) names red, green, refactor and treats the failing test as the working specification.1 Beck's case rests on a small economic claim: every minute spent writing the test before the code is paid back many times over by the regressions never re-shipped and the design pressure pushes units toward a size that can be tested at all. The 2003 book framed it as a discipline; Extreme Programming Explained (Beck, 1999) had already placed it inside a wider practice.6

The typed-language flavour belongs to Edwin Brady. Type-Driven Development with Idris (Manning, 2017) extends the cycle by adding a “types first” preface: define the type, leave the body as a hole, let the type-checker reject the inconsistencies the test would have caught, then write the test for the residue, then the body.7 Scott Wlaschin's Domain Modeling Made Functional (Pragmatic Bookshelf, 2018) and the broader F# community make the same case for an industrial typed stack;8 Yaron Minsky's “Make Illegal States Unrepresentable” pushes the claim into OCaml.9 The merger of the two traditions is what new P1 names: in dynamically typed languages the test is the first spec. In typed languages the type is and the test follows.

Steve Freeman and Nat Pryce's Growing Object-Oriented Software, Guided by Tests(Addison-Wesley, 2009) is the bridge between the unit-scale and the work-scale.10 Their outside-in TDD starts with an end-to-end acceptance test, an AC-shaped artefact, and works inward through unit tests until the implementation arrives at the bottom. That is the cleanest in-codebase example of how P1 (test-first at the code scale) and P2 Spec-First Execution (AC-first at the work scale) compose into one discipline.

The agent loop is the recent addition. Karpathy's “Verifiability” (2025) names the precondition that makes any of this work for an LLM: the task must be resettable, efficient, and rewardable.4 A failing test satisfies all three. Anthropic's evaluator-optimizer pattern, the Reflexion paper, Self-Refine, and CRITIC are the published versions of the same observation: agents grounded in machine-checkable feedback improve. Agents that grade themselves do not.2 The 2024 Huang et al. paper is the counter-evidence (without external grounding, asking GPT-4 to self-review decreases accuracy) and is the load-bearing reason the test must be runnable, not aspirational.

§2

Quotes

Write a failing automated test before you write any code. Remove duplication.

Kent Beck · Test-Driven Development by Example (2003)

As an acceptance test, it must run end-to-end to give us the feedback we need about the system’s external interfaces, which means we must have implemented a whole automated build, deploy, and test cycle.

Steve Freeman & Nat Pryce · Growing Object-Oriented Software (2009)

We put the type first, treating it as a plan for a program, and use the compiler and type checker as our assistant, guiding us to a complete and working program that satisfies the type.

Edwin Brady · Type-Driven Development with Idris (2017)

If a task/job is verifiable, then it is optimizable directly or via reinforcement learning, and a neural net can be trained to work extremely well.

Andrej Karpathy · Verifiability (2025)
§3

Evidence

19 external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Kent Beck · 2003
    The book that named red-green-refactor and put a working example next to every claim. The case for the failing test as the working specification is built page by page; the Preface names where TDD falls short (security software, concurrency), and Chapter 32, “Mastering TDD”, takes up what you don’t have to test.
  2. 02
    Kent Beck · 1999, 2004
    TDD lives inside XP. Pair programming, ten-minute build, continuous integration, and small releases are the surrounding practices that make test-first economic for the original cohort.
  3. 03
    David Astels · 2003
    The early companion volume to Beck. Walks through TDD in Java with a level of operational detail Beck’s book deliberately leaves out: the canonical second source for the discipline.
  4. 04
    Steve Freeman & Nat Pryce · 2009
    Outside-in TDD: start with the failing acceptance test, work inward through unit tests, end at the implementation. The bridge book between code-scale TDD and work-scale Spec-First.
  5. 05
    Edwin Brady · 2017
    Beck for typed languages. Define the type first, leave the body as a hole, let the type-checker reject inconsistencies before any test runs. The typed-language flavour the merged P1 carries.
  1. 06
    Scott Wlaschin · 2018
    Industrial F# treatment of type-first design. Models the domain in the type system before any test fires. The test then covers the residue the type cannot capture.
  2. 07
    Dan Vanderkam · 2019
    TypeScript-flavoured type-test-implement: items 14, 22, and 38 build the case that types are tests the compiler runs for free and the unit test suite covers the behaviour the type cannot.
  3. 08
    Vladimir Khorikov · 2020
    The book-length answer to Coplien. Test the public contract rather than the internals. The unit’s contract is the test’s contract. Saves TDD from its over-extraction failure mode without abandoning the discipline.
  4. 09
    Martin Fowler · 2012
    Sharper than the headline reads. Coverage targets are a smell. The goal is confidence and the metric is whether the team feels safe releasing. Pairs with P3 Definition of Done as the realistic-coverage tenet.
  5. 10
    Kent Beck · 2024
    Beck’s own reply to the over-extraction critique two decades later. Tidyings and behaviour changes belong in different commits. The test drives the behaviour change rather than the tidying.
  6. 11
    Martin Fowler · 2007
    The classicist vs mockist distinction. Names the failure mode TDD critics complain about, interaction-test cascades, and presents the discipline as an option, not the only option.
  7. 12
    James Coplien · 2014
    The sharpest published counter to TDD. High test mass on low-value internals is real cost. The reply is to test the public contract, not to abandon the discipline.
  8. 13
    David Heinemeier Hansson · 2014
    DHH’s essay that triggered the Beck/Fowler/DHH debates. Argues TDD pushes architecture toward over-extracted hexagons. Useful steelman for the discipline-as-religion failure mode.
  9. 14
    Andrej Karpathy · 2025
    Software 2.0 automates what you can verify: tasks must be resettable, efficient, and rewardable. Names the AI-era reason a failing test is the cheapest, most-portable reward signal a developer ever produced.
  10. 15
    Noah Shinn et al. · 2023
    Empirical case for AI-grounded TDD. HumanEval pass@1 lifts from ~80% to 91% when the agent grounds self-feedback in test runs. The signal that justifies the “tests-first for the agent” claim.
  11. 16
    Aman Madaan et al. · 2023
    Generalises Reflexion. Iterative refinement against an external signal outperforms one-shot generation across many tasks. The test suite is the canonical external signal in code work.
  12. 17
    Zhibin Gou et al. · 2024
    Tool-interactive critiquing. The verification signal must come from running tools (tests, linters, type-checkers) not from the model’s own introspection. The operational version of Karpathy’s thesis.
  13. 18
    Jie Huang et al. · 2024
    Counter-evidence used positively. Without external grounding, asking GPT-4 to self-review decreases accuracy. The load-bearing reason the test must be runnable, not aspirational.
  14. 19
    Anthropic · 2024
    Names the evaluator-optimizer pattern: one LLM generates, another (or an external check) evaluates. The test runner is the canonical evaluator; P1 is what gives the loop something to grade.

Nineteen sources, three stances. The supporters are Beck, Astels, Freeman & Pryce and Brady, the canon for dynamic and typed languages alike. The qualifiers further down hold the “TDD is a discipline rather than a religion” line. The opposers argue against the brittleness of the suite as a design medium: the steelman the reply has to address.

§4

Examples

Viewing: TypeScript.
Avoid

Before implementation first; the test confirms what already shipped.

Filerescue-hedgehog.ts
function rescueHedgehog(hedgehog: Hedgehog, sanctuary: Sanctuary): RescueResult {
  if (sanctuary.intake.length >= sanctuary.capacity) {
    return { ok: false, reason: "full" };
  }
  sanctuary.intake.push(hedgehog);
  return { ok: true };
}

it("rescues a hedgehog", () => {
  expect(rescueHedgehog(hg, sanctuary).ok).toBe(true);
});
Prefer

After failing test first; the smallest code that turns it green.

Filerescue-hedgehog.spec.ts
it("rejects rescue when the sanctuary is at capacity", () => {
  const full: Sanctuary = { capacity: 2, intake: [hg1, hg2] };
  const result = rescueHedgehog(arrival, full);
  expect(result).toEqual({ ok: false, reason: "full" });
});

function rescueHedgehog(arrival: Hedgehog, s: Sanctuary): RescueResult {
  if (s.intake.length >= s.capacity) return { ok: false, reason: "full" };
  s.intake.push(arrival);
  return { ok: true };
}
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
vitest/expect-expect@vitest/eslint-plugintests with no assertion: passes silently because nothing is checked.
vitest/no-disabled-tests@vitest/eslint-plugintests left as it.skip / xit on the main branch.
vitest/no-focused-tests@vitest/eslint-pluginit.only / fit committed accidentally: silently shrinks the suite.
vitest/no-conditional-tests@vitest/eslint-plugintests inside a runtime conditional: the assertion may never run depending on environment.
vitest/require-top-level-describe@vitest/eslint-pluginloose tests with no describe block: makes the spec output unreadable and breaks behaviour-name conventions.
vitest/valid-title@vitest/eslint-plugintest titles that aren’t strings or are duplicates within a file.
tsc --noEmitTypeScript compilerthe type-as-first-test phase. Run on save and pre-push; with strict on, a compile failure is a failed test.
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import vitest from '@vitest/eslint-plugin';

export default defineConfig({
  files: ['**/*.{ts,tsx}'],
  plugins: { vitest },
  rules: {
    'vitest/expect-expect': 'error',
    'vitest/no-disabled-tests': 'error',
    'vitest/no-focused-tests': 'error',
    'vitest/no-conditional-tests': 'error',
    'vitest/require-top-level-describe': 'error',
    'vitest/valid-title': 'error',
  }
});
§4c

AI rules

File.cursor/rules/p1-test-first.mdc
---
alwaysApply: true
---

## Prickles P1: Test-First Development

Write the failing test before the implementation. Watch it fail for the reason you predicted. Then write the smallest code that makes it green.

In typed languages, the type is the first test. Define the signature, leave the body as a hole, let the compiler reject the inconsistencies the unit test would have caught, then write the test for the residue.

Test the public contract rather than the internals. The unit's contract is the test's contract. Mock cascades through private collaborators are the smell, not the rule.

Beck's exceptions still apply: spikes, throwaway demos, declarative config, and generated code are tested via the surrounding code, not as units in their own right.

Repo layout, CI, and ESLint wiring for these paths live on /implementation, not repeated on every tenet.

§5

Counter-argument

Counter

The honest steelman is Coplien's, sharpened by DHH: the test suite written first becomes a cage. The team optimises for the granular, the unit-shaped, the easy-to-mock. The suite calcifies around an early architecture and refactoring becomes a rewrite of the tests rather than the code.3 Hillel Wayne extends the point: not all behaviour reduces to a runnable assertion;5 some properties (concurrency, distributed timing, performance under load) are cheaper to model-check than to test. James Coplien's “Why Most Unit Testing Is Waste” is the spiciest version of the argument and has not been fairly answered by anyone who skips its central claim: that high test mass on low-value internals is real cost, not virtue.

§6

Counter-argument retort

Reply

Coplien aims well but hits the wrong target. The brittleness he describes is real and it is the brittleness of too many low-value unit tests on internals, not of writing tests first. The fix is not to abandon test-first. It is to write the test against the public surface that F6 Encapsulation tells you to expose, and to let coupling pull the rest along. Khorikov's Unit Testing Principles (2020) makes the case in detail: the unit's contract is the test's contract;11 internal-mock cascades are the smell, not the rule.

Hillel Wayne's extension is the genuine residue. Not everything reduces to a runnable assertion: concurrency, distributed timing, and load-shaped behaviour are cheaper to model-check than to test, which is why TLA+ exists.5 The reply is to keep both in the toolkit and not pretend tests cover everything. Property-based testing covers the algebraic residue. Model-checking covers the temporal residue. The test-first discipline still applies to the bulk of behaviour-shaped code where it lives.

DHH's critique, that test-driven design is a religion producing over-extracted hexagons, is right when the practitioner mistakes the discipline for an architectural style. Beck's own phrase in Tidy First? (2024) is the calm reply:12 tidyings and behaviour changes are different commits. The test drives the behaviour change rather than the tidying. The hexagon is one option among many. The red-green-refactor cycle is agnostic to the architecture it operates inside.

The discipline survives the agent. In fact the agent loop hardens it: an LLM with a failing test in front of it is grounded. An LLM without one is generating plausible prose at machine speed. The literature on this dates from 2023 and 2024 and is unanimous: Reflexion, Self-Refine, CRITIC, evaluator-optimizer all show the same shape.2 The exceptions Beck himself names (spikes, throwaway demos, declarative config, generated code) remain exceptions. They are footnotes on the rule, not replacements for it.

§7

Notes

  1. [1]Kent Beck: Test-Driven Development by Example (Addison-Wesley, 2003). The canonical statement of red-green-refactor and the case for the failing test as a working specification. Test-Driven Development by Example (Addison-Wesley, 2003). The canonical statement of red-green-refactor and the case for the failing test as a working specification. The Preface names work that tests alone cannot drive, security software and concurrency among them, and Chapter 32, “Mastering TDD”, takes up what you don’t have to test.
  2. [2]Noah Shinn et al.: “Reflexion: Language Agents with Verbal Reinforcement Learning”, NeurIPS 2023. HumanEval pass@1 lifts from ~80% to 91% when the agent grounds self-feedback in test runs rather than pure introspection: empirical evidence that machine-checkable rewards drive agent improvement. Generalised by Madaan et al. (Self-Refine, 2023) and Gou et al. (CRITIC, 2024).
  3. [3]James Coplien: “Why Most Unit Testing Is Waste” (RBCS, 2014). The sharpest published counter to TDD: high test mass on low-value internals is real cost. The reply is not that Coplien is wrong about the failure mode but that the fix is to test the public contract, not to abandon the discipline.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-10-13