SPEC WRITINGTEMPLATE

Test Plan Spec

Ask an agent to "add tests" and you will get tests. Lots of them, green, fast, and aimed wherever the code was easiest to aim at. Coverage of the function that formats dates: extensive. Coverage of the checkout flow that takes money: one test, mocked so thoroughly it would pass if the payment provider ceased to exist. The agent did what you asked. You asked for tests when you meant protection, and those are different orders.

A test plan is you deciding what needs protecting before anyone writes a test. The structure is deliberately small: three to five happy paths, three to five edge cases, one or two failure modes. Happy paths are the promises on the tin: the thing a user came to do, working start to finish. Edge cases are the borders: empty, maximum, duplicate, zero, the unicode name, the second click on a button that should only work once. Failure modes are the world misbehaving: the API that doesn't answer, the file that doesn't parse. You are not aiming for coverage of everything. You are aiming for a written answer to "if this breaks tonight, what did we lose?"

The other half of the plan is defining "passing," and it is less obvious than it sounds. A test that asserts the function returns something is green whether the answer is right or garbage. Every case in the plan states its expected outcome concretely, so the agent has to assert against reality instead of against existence. And one rule carries most of the weight of the whole document: when fixing a bug, the test gets written first and must fail. A test you never saw fail has told you nothing about whether it can.

markdown
# Test Plan: [feature or project name]

Scope: [what this plan covers, e.g. "the invoice creation flow" —
one feature per plan, keep it small enough to actually maintain]
Test framework: [pytest / vitest / etc.]
Run with: [exact command, e.g. `pytest tests/test_invoices.py -v`]

## Happy paths (3-5)

[The core promises. Each: a name, the setup, the action, and a concrete
expected outcome. "It works" is not an outcome. A number, a state, or an
exact message is an outcome.]

1. **[Create an invoice]**
   - Setup: [logged-in user with one customer on file]
   - Action: [create an invoice with two line items, $40 and $60]
   - Passes when: [invoice exists with status "draft" and total exactly
     $100.00, and it appears in that customer's invoice list]

2. **[Second core flow]**
   - Setup: []
   - Action: []
   - Passes when: []

3. **[Third core flow]**
   - Setup: []
   - Action: []
   - Passes when: []

## Edge cases (3-5)

[The borders of normal. Empty, biggest allowed, duplicate, zero,
strange-but-legal input. For each, decide the intended behavior HERE:
if you don't, the test will simply enshrine whatever the code happens
to do, bug included.]

1. **[Empty state]**: [action on a brand-new account with no data] →
   passes when [friendly empty message, not an error, not a blank screen]
2. **[Boundary]**: [e.g. line item of $0 / title at exactly the max length] →
   passes when [state the rule: accepted, or rejected with a specific message]
3. **[Duplicate action]**: [e.g. submit clicked twice fast] →
   passes when [exactly one record exists afterward]
4. **[Awkward-but-legal input]**: [unicode name, 3MB note, negative quantity] →
   passes when [your decision]

## Failure modes (1-2)

[The world breaking around the code. These tests simulate the failure
(mock the dead API, feed the corrupt file) and assert the app degrades
on purpose instead of by accident.]

1. **[Dependency down]**: [e.g. email service times out during send] →
   passes when [invoice still saves, user sees "saved, email failed, retry
   available", and NO data is lost or half-written]
2. **[Bad data in]**: [e.g. imported CSV has a malformed row] →
   passes when [good rows import, bad row reported by line number,
   nothing silently dropped]

## What counts as passing (the bar for "done")

- Every case above is automated and green via the run command at the top.
- Each test asserts the concrete outcome written here, not merely that
  the code ran without throwing.
- Tests are deterministic: same result every run, no sleeps, no reliance
  on today's date or call order. A flaky test is a broken test.
- Tests use their own test data, never the real database.

## Rules for the agent

- For bug fixes: write the failing test first, show me that it fails,
  then fix. A test born green proves nothing.
- Never weaken an assertion or delete a test to get to green. If a test
  seems wrong, say so and stop; the plan changes by discussion, not erosion.
- If code can't be tested without contortions, flag it: hard-to-test is
  a design smell, and I want to know about it, not have it worked around.

## Manual checks (the 5 minutes before anything ships)

[Things automation misses. Run these yourself, by hand.]

- [ ] Click through happy path 1 in a real browser.
- [ ] Check it on a phone-sized screen.
- [ ] [The one thing this feature would be most embarrassing to break.]

Adaptation notes:

  • Write the plan before the feature exists and it doubles as a spec: the happy paths are your requirements with verification bolted on. This is the cheapest moment to discover you and the agent imagine the feature differently.
  • One plan per feature beats one heroic plan per project. Small plans get maintained; the heroic one gets admired and abandoned.
  • The "passes when" decisions on edge cases are the real work in this file. Deciding what a $0 line item should do takes a project owner; typing the test afterward takes an agent about a minute.
  • For code with no UI, the manual-checks section shrinks but rarely to zero: reading one real output file with your own eyes catches the category of wrong that assertions weren't written for.
  • The classic mistake is measuring this plan in test count or coverage percentage. Twelve tests aimed by this structure protect more than a hundred aimed at whatever was convenient, and the agent will happily generate the hundred if you let it.