Skip to content
← All guides

Tests are how you go faster with an agent, not slower

6 min read

Tests are what let the agent self-correct without you in the loop, turning ten human turns into one review.

Skipping tests to move faster is the belief worth attacking head-on, because with an agent it produces precisely the opposite result. Without tests, you are the test runner. Every iteration has to come back through you, and you are the slowest component in the loop by two orders of magnitude.

The intuition it contradicts was earned honestly. When you write code by hand, tests are more code you write by hand, and skipping them genuinely does get version one out sooner. When something else writes the code, that arithmetic changes.

The two loops, step by step

Without tests:

  1. You describe what you want.
  2. The agent writes the code and reports that it looks correct.
  3. You read the diff, or run it, or click through the app.
  4. You find the bug.
  5. You write a sentence describing the bug.
  6. Back to step two.

Steps three, four and five are you. Every iteration costs a human turn: a context switch, a diff read, a sentence composed. Ten iterations is ten interruptions.

With tests:

  1. You describe what you want, in a test.
  2. The agent writes the code.
  3. The agent runs the test.
  4. It fails. The agent reads the failure and changes the code.
  5. Back to step three, without you.
  6. It passes. Now you review.

Your turn appears once, at the end, on code that already satisfies the specification. The number of steps did not shrink. They stopped needing a human.

That is the reframe: a test is a machine-checkable specification. It converts "looks right" into "is right" in a form the agent can execute on its own, which is the only mechanism by which an agent self-corrects. An agent with no way to check its own work will tell you the change is complete with total confidence, because from where it sits there is no evidence to the contrary.

What to test when you are moving fast

Not everything. Three categories pay for themselves immediately:

  • The contract of each module. What goes in, what comes out, what happens at the boundaries. This is also what the agent reads later to work out how to call the module.
  • Invariants that are expensive to get wrong. Money never rounds in your favour. A user cannot read another tenant's rows. An order cannot ship twice. These are the tests you would want if you were told a stranger would rewrite the file next week, which is roughly the situation you are in.
  • Every bug, the moment it is fixed. Failing test first, then the fix. It is the cheapest test you will ever write because the reproduction is already in your head, and it is the one that stops the same bug returning in a future session where the agent has no memory of the first one.

Not on the list: getters, thin wrappers, framework glue, and anything whose test would only restate the implementation.

Behaviour, not implementation

The characteristic failure of agent-written tests is one that passes by construction. It mirrors the implementation line for line, so it can never catch the implementation being wrong.

import { it, expect } from "vitest";
import { splitBill } from "./split-bill";

// Restates the implementation. Passes even when the maths is wrong.
it("divides the total by the number of people", () => {
  expect(splitBill(1000, 3)).toBe(Math.floor(1000 / 3));
});

// Asserts on behaviour anyone actually cares about.
it("splits without losing or inventing pennies", () => {
  const shares = splitBill(1000, 3);
  expect(shares).toEqual([334, 333, 333]);
  expect(shares.reduce((a, b) => a + b, 0)).toBe(1000);
});

The second test fails if someone rounds each share independently and returns 999 pennies from a 1000 penny bill. The first does not, because it computes its expected value the same way the code does. That is the tell when reviewing agent-written tests: an expected value derived rather than stated.

Characterisation tests for code you did not read

Refactoring code you did not write and have not read is now the common case. The technique that makes it safe predates agents and suits them perfectly: capture what the code does today, before you touch it.

it("characterises current pricing behaviour", () => {
  // Not a specification. A record of what this does today, written
  // before the refactor so the refactor cannot quietly change it.
  expect(price({ plan: "pro", seats: 3, coupon: null })).toBe(8700);
  expect(price({ plan: "pro", seats: 3, coupon: "HALF" })).toBe(4350);
  expect(price({ plan: "free", seats: 99, coupon: "HALF" })).toBe(0);
});

These are not aspirational. They record current behaviour including behaviour you suspect is wrong, and that is the point. A refactor is meant to change structure and not behaviour, so anything that moves is a signal. Have the agent write these first, run them against the untouched code to confirm they pass, commit them, and only then start the refactor.

If one turns out to encode a genuine bug, you now have a deliberate decision to make, with a test stating exactly what happens today. More on that read itself in Reviewing code you did not write.

Write the test yourself, let the agent write the implementation

The highest-leverage manual work available to you is writing the test. The test is where intent gets encoded, and intent is the one thing an agent cannot infer from the codebase. Implementation is mechanical by comparison, and agents are genuinely good at mechanical.

Practically: write the assertions yourself, or at minimum the cases, and let the agent fill in setup and mechanics. Then review the test more carefully than the code, because from that moment on the test is what the code is checked against. A wrong test is worse than no test, since it turns a real failure into a green tick.

Forbid editing tests to make them pass

This is the guard rail everything above depends on. An agent stuck on a failing test has two exits, and one of them is changing the test. It will not present that as cheating; it will describe it as correcting an incorrect expectation, and the reasoning reads plausibly at a glance.

Paste something close to this into your project instruction file:

## Tests
When fixing an implementation, do not modify test files. If you believe a
test is wrong, stop and say so: name the test, quote the assertion, and
state what you think the correct expectation should be. Wait for
confirmation. Tests may only be edited when the task is explicitly
about changing them.

The "stop and say so" clause carries as much weight as the prohibition. Sometimes the test really is wrong, and you want that surfaced rather than silently repaired. What you remove is the option to settle the disagreement unilaterally.

Suite runtime is iteration time

Once the agent runs tests on its own, your suite's runtime becomes the agent's iteration time directly. A ninety-second suite means every self-correction cycle costs ninety seconds, and a task needing six cycles spends nine minutes waiting.

Give it a targeted command and a fast subset, and name both in the instruction file:

# While iterating: the one relevant file
pnpm vitest run src/features/checkout/total.test.ts

# Fast subset: unit tests only, no database, no network
pnpm vitest run --project unit

# Everything, integration included: end of task and CI
pnpm vitest run

Then tell the agent explicitly: run the single file while working, the full suite once at the end. Left alone it will run everything on every attempt, and a good share of the feeling that the agent is slow comes from that.

Rule of thumb: if you cannot get a relevant pass or fail in under ten seconds, your bottleneck is the suite, not the model.

testingqualityworkflowfeedback-loops