# Spec First: Tests Before Code

Course: [Testing & Verification](https://www.devtutorials.ai/learn/testing-verification) · Section: Verifying AI Changes
Source: https://www.devtutorials.ai/learn/testing-verification/verifying-ai-changes/spec-first

> Turn a ticket into acceptance criteria and tests before the agent writes code. See every new test fail on the old code first, then pass, and catch edge cases hidden in words like "or more".

## What Spec First Means

**Spec** = a short, clear description of what the code must do. On a team, it is often the ticket.

**Spec first** = you write the tests from the spec **before** the agent writes the code.

Why do it in that order?

- If the agent writes the code first, its tests often copy what the code does, bugs included.
- If you write the tests first, they come from what the code **should** do.
- Then the agent's code has to meet your tests, not the other way round.

**Simple lesson:** tests from the spec judge the code. Tests from the code only repeat it.

### A Real-World Example

Think about a school exam.

- The teacher writes the questions and the answer key **before** the exam.
- Students then write their answers.
- The teacher marks them against the key.

Now imagine the teacher writes the answer key **after** reading one student's paper. The key would just copy that student's mistakes.

Tests are the answer key. Write them first.

- **Test** = code that checks other code gives the right answer.
- **Arrange, Act, Assert** = set up the input, run the code, check the answer with `expect(...)`.
- **Green** = every check passed. **Red** = at least one check failed.

The full card is in **Done Is a Claim**.

## What You Will Learn

In this lesson, you will learn:

1. How to turn a ticket into acceptance criteria.
2. How to write one test per criterion.
3. What **fail first** means, and why a test must be red on the old code.
4. The red, green loop with an AI agent.
5. How edge cases hide inside "or more" and "never more than".

## From Ticket to Tests

### Acceptance Criteria

**Acceptance criteria** = the list of rules that must be true before a ticket is done.

Here is the ticket for this lesson:

- **SHOP-42:** add a SAVE10 coupon.
- SAVE10 gives 10% off orders of ₹500 or more.
- Orders under ₹500 pay the full price.
- The discount is never more than ₹200.
- The code works in any case: save10, Save10, SAVE10.
- Any other code throws the error "Unknown coupon".

Each line is one rule. Each rule becomes at least one test.

**A good criterion has a clear input and a clear answer.**

- Testable: "₹1000 with SAVE10 costs ₹900."
- Too vague: "The coupon should work well."
- If you cannot write `expect(...).toBe(...)` for it, ask for a clearer rule.

### One Criterion, One Test

Here is the test for criterion 1. Open `TICKET.md` in the explorer to see all five.

Press **▶ Run tests**. It passes on the new code.

Now press **↺ Run on old code**.

- **The Tests tab:** red on the old code. The old code returns ₹1000.
- **The Evidence tab:** rung 4, "Tests fail on the old code first", lights up.

**Simple lesson:** one criterion, one test, with a number you worked out yourself.

## Fail First

### Why a Test Must Be Red First

**Fail first** = a new test must fail on the code before the change, then pass after it.

- If it fails first, you know the test can **see** the change.
- If it passes on the old code too, it is not testing the new rule.

<figure className="ver-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="ver-fig-redgreen" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="ver-fig-redgreen">Animation: the loop of spec first. Write the test, run it on the old code and see red, the agent writes the code, run again and see green.</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>
    <rect x="20" y="60" width="130" height="60" rx="12" style={{ fill: "var(--ver-blue-soft)", stroke: "var(--ver-blue)" }} />
    <text x="85" y="95" textAnchor="middle" style={{ fill: "var(--ver-blue)" }}>1. Write test</text>
    <rect x="175" y="60" width="130" height="60" rx="12" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }} />
    <text x="240" y="88" textAnchor="middle" style={{ fill: "var(--ver-red)" }}>2. Old code</text>
    <text x="240" y="106" textAnchor="middle" style={{ fill: "var(--ver-red)" }}>✗ red</text>
    <rect x="330" y="60" width="130" height="60" rx="12" style={{ fill: "var(--ver-slate-soft)", stroke: "var(--ver-slate)" }} />
    <text x="395" y="88" textAnchor="middle" style={{ fill: "var(--ink)" }}>3. Agent writes</text>
    <text x="395" y="106" textAnchor="middle" style={{ fill: "var(--ink)" }}>the code</text>
    <rect x="485" y="60" width="130" height="60" rx="12" style={{ fill: "var(--ver-green-soft)", stroke: "var(--ver-green)" }} />
    <text x="550" y="88" textAnchor="middle" style={{ fill: "var(--ver-green)" }}>4. New code</text>
    <text x="550" y="106" textAnchor="middle" style={{ fill: "var(--ver-green)" }}>✓ green</text>
  </g>
  <path d="M150 90 H173 M305 90 H328 M460 90 H483" style={{ stroke: "var(--ver-slate)", strokeWidth: 2 }} />
  <circle r="8" cy="90" style={{ fill: "var(--accent)" }}>
    <animate attributeName="cx" values="85;240;240;395;395;550;550" keyTimes="0;0.15;0.35;0.5;0.65;0.8;1" dur="7s" repeatCount="indefinite" />
  </circle>
  <text x="320" y="160" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>Red first proves the test can see the change. Green after proves the change works.</text>
</svg>
<figcaption>Red, then green. Both runs are evidence.</figcaption>
</figure>

### A Test That Cannot See the Change

This test checks criterion 2: ₹100 pays the full price.

Press **▶ Run tests**, then **↺ Run on old code**.

- **Both runs:** green.
- **Why:** the old code already returned the full price for everything.
- **What it means:** this test alone cannot tell old code from new code. Keep it, but add tests that fail first.

**Green on the old code is a warning.**

- Some tests are fine as guards. They protect behaviour that should not change.
- But every **new** rule needs at least one test that is red on the old code.

**Simple lesson:** a test that never failed has never proved anything.

## The Agent Writes the Code

### Red Shows What Is Missing

You gave the agent your tests. Here is its first try.

Press **▶ Run tests**.

- **The Tests tab:** 1 passed, 1 failed. "expected 4500 to be 4800".
- **Why:** the agent forgot criterion 3, the ₹200 limit.
- **Now try:** in `src/coupon.ts`, wrap the discount: `Math.min(Math.round(total * 10) / 100, 200)`. Run again. Green.

### Edges Hide in the Words

Words like "or more", "at least" and "never more than" hide an **edge**.

**Edge case** = an input right on the line between two rules.

- "₹500 or more" means ₹500 itself gets the discount.
- So test ₹500 and ₹499, not just ₹1000 and ₹100.

Press **▶ Run tests**.

- **The Tests tab:** red. "expected 500 to be 450".
- **Why:** the code uses `total <= 500` where it should use `total < 500`.

**For every limit in a ticket, test three values:**

- just below (₹499)
- exactly on it (₹500)
- just above (₹501)

**Simple lesson:** the agent's code passes your tests, so your tests must say everything the ticket says.

## Did Your Tests Cover Every Criterion?

Two tests are not enough for five rules. Plant the hidden bugs and see.

Press **🐞 Plant bugs**.

- **The Bug check tab:** 0 of 5 caught.
- **Why:** criteria 3, 4 and 5 have no test. The ₹500 edge has no test either.
- **Now try:** add one test for each missing rule. Plant the bugs again.

**Simple lesson:** count your criteria and count your tests. Every rule needs a check.

## Use It at Work

- **Write the tests before you ask the agent.** One test per acceptance criterion.
- **See them fail on the current code:** `npx vitest run tests/coupon.test.ts`. Red is good here.
- **Then hand them over:**

```text
These tests describe the feature. Make them pass.
Do not edit any file in tests/. If a test looks wrong, stop and tell me why.
```

- **Fail-first on a finished branch:** run your new tests on `main`.

```bash
git stash push -- src      # put back the old source for a moment
npx vitest run tests/coupon.test.ts   # should be red
git stash pop              # bring the new source back
```

## TL;DR

1. **Spec** = a clear description of what the code must do, often the ticket.
2. **Acceptance criteria** = the rules that must be true. One test each, at least.
3. **Spec first** = write the tests before the agent writes the code.
4. **Fail first** = a new test is red on the old code, then green on the new code.
5. **Edge case** = an input right on a limit. Test below, on and above it.
6. **The loop:**
    - Write tests from the ticket.
    - Run on the old code: red.
    - Agent writes the code.
    - Run again: green.
    - Plant bugs to check the tests.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Code first, tests after | "Let the agent write both." | Write the tests from the ticket first. | The tests copy the code's bugs. |
| Never seeing red | "It passed, so the test works." | Run it on the old code. It must fail first. | The test cannot see the change at all. |
| Vague criteria | "Make the coupon work well." | Give an input and an exact answer. | Nobody can test it, so nobody does. |
| Skipping edges | "₹1000 and ₹100 cover it." | Test ₹499, ₹500 and ₹501. | `<=` vs `<` slips through. |
| One test for five rules | "One happy test is enough." | One test per criterion. | Missing rules ship as bugs. |

## Check Your Understanding

## Exercises

## What to Remember

- Write the tests from the ticket before the agent writes the code.
- One test per acceptance criterion, with numbers you worked out.
- Every new test must fail on the old code first.
- Test the edge: below, on and above every limit.
- Plant bugs to check the tests cover every rule.

## Further Reading

- [Vitest getting started](https://vitest.dev/guide/): set up the runner in your own project.
- [Test-driven development, by Martin Fowler](https://martinfowler.com/bliki/TestDrivenDevelopment.html): the red, green, refactor loop.

Next: **Reviewing AI Code**. You will review six AI changes with a checklist and catch made-up APIs, off-by-one errors and changes nobody asked for.
