Specs and tests as the contract

Write the requirement first, turn it into a failing test, and let the agent drive it to green - including the edge cases it would otherwise skip.

The requirement comes first

A test written before the code is a specification the agent cannot negotiate with. A test written after is a description of whatever the agent happened to build, which is a very different document and much less valuable.

Requirement: a discount code applies once per customer.

Acceptance criteria:
- Applying a used code returns 409 with code ALREADY_REDEEMED.
- Applying an expired code returns 409 with code EXPIRED.
- Applying an unknown code returns 404.
- The discount is applied to the subtotal, before tax.
- Codes are case-insensitive and trimmed.
  1. Write the criteria before the prompt. Half of them will be decisions you had not actually made.
  2. Turn each criterion into a failing test - or ask the agent to, and then read the tests before any implementation exists.
  3. Only then ask for the implementation, with the failing test as the target.
  4. Run the suite and require real output, not a claim.
  5. Delete any test the agent added that does not correspond to a criterion.
💡
Read the tests before you read the implementation. A wrong test is much harder to spot once there is a working implementation sitting next to it, because green tests create a strong pull to believe them.

Red to green

import { test, expect } from "vitest";
import { applyDiscount } from "./discount";

test("rejects a code that has already been redeemed", () => {
  const result = applyDiscount({ code: "SAVE10", redeemedBy: ["user_1"] }, "user_1");
  expect(result).toEqual({ ok: false, error: "ALREADY_REDEEMED" });
});

test("trims and lowercases the code before lookup", () => {
  const result = applyDiscount({ code: "  save10  " }, "user_2");
  expect(result.ok).toBe(true);
});
npm test -- discount.test.ts
# FAIL  src/discount.test.ts
# Error: Cannot find module './discount'

# now, and only now:
> "Implement src/discount.ts so these tests pass. Do not modify the tests."
  • The red step proves the test can fail. A test that passes before the code exists is testing nothing.
  • 'Do not modify the tests' is worth saying out loud; without it the shortest path to green may run through the assertions.

Edge cases agents skip

CategoryAsk for
EmptyNo items, empty string, empty list
BoundaryZero, one, maximum, off-by-one at a limit
DuplicatesThe same value twice, repeated calls
OrderingInput in an unexpected order
Unicode and caseAccented characters, mixed case, trimming
ConcurrencyTwo requests arriving together
FailureNetwork error, timeout, malformed response
PermissionThe same action as a different role
"Now add tests for the cases you would expect to break:
two simultaneous redemptions of the same code, a code with leading
whitespace, and a user who has never redeemed anything.
Report any case where you had to guess the correct behaviour."

That last instruction is the useful one. Cases where the agent had to guess are exactly the cases where the requirement was underspecified, and they are cheaper to settle now than after the code is deployed.

# a mutation check: do the tests actually bite?
npx stryker run --mutate src/discount.ts

# or simply break the code by hand and confirm a test fails
# if nothing fails, the test is decoration

FAQ

Is it not slower to write tests first?
It is slower for the first ten minutes and much faster afterwards, because it replaces several rounds of 'that is not quite what I meant' with a single executable statement. The cost is real; the saving is in the review and the rework.
How do I know the tests are meaningful?
Break the implementation deliberately and confirm a test fails. If you can delete a line of business logic without any test going red, the suite is not yet a contract - it is a description of the happy path.

The generate, run, correct loop Types, linters and static checks as the first filter

Last refreshed 2026-09-18.