If you want to write unit tests with AI, the easy part is getting tests. Ask any coding agent for “tests for this module” and you will have forty green tests in a few minutes. The hard part is getting tests that fail when the code is wrong. An agent that reads a function and then writes assertions about what that function returns has mostly written the function a second time.
This guide covers how to get useful tests out of coding agents like Claude Code, Codex or Gemini CLI: what to ask for, how to spot tests that prove nothing, how to measure them with coverage and mutation testing, and how to split the work so the agent that writes the code can’t quietly bend the tests.
Why AI-written tests go wrong
Agents are very good at the mechanical parts of testing: fixtures, factories, mocks, parameterized cases, the boilerplate for a new test file. Where they slip is the thing that makes a test valuable, the expected value.
When an agent writes a test, it usually has the implementation open in its context. The natural move is to read the code, work out what it returns and assert exactly that. If the code has a bug, the test now locks the bug in. It’s green, it covers the lines, and it would stay green if you never fixed the bug.
There are a few common shapes of this:
- Mirrored logic. The test computes the expected value with the same formula as the code:
expect(price(100, 0.2)).toBe(100 - 100 * 0.2). If the formula is wrong, both are wrong together. - Mocking the subject. The thing under test is mocked, so the test checks the mock.
- Call-only assertions.
expect(sendEmail).toHaveBeenCalled()with no check of what was sent or to whom. - Snapshot everything. A huge snapshot nobody reads, updated with
-uwhenever it fails. - Assertions that can’t fail.
expect(result).toBeDefined()on a function that always returns an object. - Editing the test to pass. The code fails a test, and the agent “fixes” the test’s expected value instead of the code.
None of these are unique to AI. People write them too. Agents just produce them faster and in bigger batches, so you need a process that catches them.
Start from behaviour, not from the code
The single biggest improvement is to tell the agent what the code is supposed to do, and to keep the expected values out of the implementation’s hands.
A weak prompt:
Write unit tests for
src/billing/discount.ts.
A better one:
Write unit tests for
applyDiscountinsrc/billing/discount.ts. Rules: a percentage discount never takes the total below zero; discounts don’t stack, the largest one wins; an expired code is ignored, not an error; amounts are rounded to 2 decimals, half up. Write the expected values by hand from these rules, not by running or reading the implementation. If a rule seems to disagree with the current code, write the test anyway and tell me which tests fail.
Three things changed. The rules come from you, or from a ticket, a spec or the docs. The expected values are literals worked out from the rules. And a failing test is allowed: it might be a real bug, which is exactly what tests are for.
A few more habits help:
- Ask for the case list first. “List the cases you plan to test, one line each, before writing any code.” You can add the edge cases it missed (empty input, unicode, time zones, the boundary at exactly zero) before any code exists.
- Name the boundaries. Agents are good at edge cases when you point at the dimension: “inputs at, just below and just above each limit”.
- Say what not to mock. “Don’t mock anything inside
src/billing; mock only the HTTP client.” Over-mocking is how many tautological tests start. - One behaviour per test. Tests with a clear name like
ignores_expired_codesare easier to review than one test with twelve assertions.
Unit, integration and end-to-end tests
The same agent can write all three kinds, but the instructions differ.
Unit tests
Fast, isolated, many of them. This is where agents save the most time. Point the agent at your existing test style (“follow tests/billing/tax.test.ts”) so you don’t get a new helper library every time. Put the test command in your CLAUDE.md or AGENTS.md so the agent runs the tests on its own; more on that in how to write CLAUDE.md and AGENTS.md.
Integration tests
These touch a real database, queue or file system. Agents tend to reach for mocks here, which turns an integration test back into a unit test. Be explicit: “use the test database from docker-compose.test.yml; don’t mock the repository layer.” Also ask for cleanup, so tests don’t depend on each other’s leftovers.
End-to-end tests
For browser tests, an agent can write Playwright or Cypress specs from a user flow you describe. Selectors are the weak point: ask for role or label based selectors (getByRole('button', { name: 'Pay' })) instead of CSS paths that break on the next redesign. The AI agent browser testing post covers letting an agent drive a real browser to check its own work.
Characterization tests for code you don’t understand yet
Sometimes there is no spec. The code is old, the author is gone and nobody knows exactly what it does. Here the goal flips: you want tests that record what the code does today, bugs included, so you notice when that changes.
These are called characterization tests, and they are one of the places where letting the agent read the implementation is the right call. The prompt changes accordingly:
Write characterization tests for
legacy/invoice_export.py. Record current behaviour, including anything that looks like a bug; don’t fix anything. Cover each branch you can find. Mark any behaviour that looks wrong with a# SUSPICIOUS:comment explaining why.
The SUSPICIOUS notes become your bug list for later. Snapshot or approval-style tests are fine here, as long as they are small enough to read. This is the first step of any safe refactor, which the refactor legacy code with AI guide walks through.
Use coverage to find gaps, not to grade tests
Coverage is a good map and a bad score. Use it to tell the agent where to look:
| Stack | Command |
|---|---|
| Vitest | npx vitest run --coverage |
| Jest | npx jest --coverage |
| pytest (with pytest-cov) | pytest --cov=myapp --cov-report=term-missing |
| Go | go test -coverprofile=cover.out ./... then go tool cover -func=cover.out |
Vitest needs a coverage provider such as @vitest/coverage-v8 installed. The term-missing report in pytest lists the exact uncovered line numbers, which is handy to paste straight into a prompt.
A good loop:
- Run coverage and pick the modules that matter: billing, auth, data migrations, not generated code.
- Give the agent the uncovered lines and ask what behaviour they represent.
- Have it write tests for that behaviour, with expected values from the rules.
- Rerun coverage to confirm the lines are now exercised.
What you should not do is say “get coverage to 90%”. An agent given a number will hit the number, and the cheapest way to do it is tests that execute lines without checking much.
Mutation testing: are the tests actually checking anything?
Mutation testing answers the question coverage can’t. A tool makes small changes to your code, such as flipping > to >=, replacing + with - or returning an empty value, and runs your tests against each change. If the tests still pass, the mutant “survived”, and you have found a spot where the tests don’t really check behaviour.
Common tools:
- JavaScript and TypeScript: StrykerJS. Set it up with
npm init stryker@latest, then runnpx stryker run. - Python: mutmut. Run
mutmut run, thenmutmut browseto go through the surviving mutants. Recent versions need fork support, so on Windows it runs under WSL. - Java: PIT, for example
mvn test-compile org.pitest:pitest-maven:mutationCoverage. - Rust: cargo-mutants, installed with
cargo install cargo-mutantsand run withcargo mutants.
Mutation runs are slow on a big codebase, so scope them to the module you are working on. Then hand the survivors to the agent:
These mutants survived in
discount.ts: (list). For each one, write a test that fails on the mutant and passes on the original. Don’t changediscount.ts.
This is one of the best uses of an agent in testing. The task is precise, the success check is mechanical, and the result is a test that provably catches a real kind of bug.
Let one agent write tests and another write code
The most reliable way to stop an agent from bending tests is to make sure the agent writing the code can’t touch them.
The setup is simple:
- Test agent. Gets the spec and writes tests in
tests/(or wherever your tests live). It doesn’t see or write the implementation. Its tests should fail at first, because the feature doesn’t exist yet. - Implementer agent. Gets the same spec plus the failing tests and writes the code until they pass. It may not edit anything in
tests/. - Disagreements go back. If the implementer thinks a test is wrong, it asks the test agent, or you, instead of editing it.
This is test-driven development split across two agents, and it works across vendors too. Some teams like one model writing the tests and a different one implementing, so the two don’t share the same blind spots.
Running two agents on the same feature needs some isolation so they don’t trip over each other. A separate git worktree per agent is the usual answer; see git worktrees for parallel agents. For this particular split you can also keep both in one worktree, since they write to different folders.
Reviewing AI-written tests
Read the tests at least as carefully as the code. A short checklist:
- Do the expected values come from the rules? Look for literals, not formulas copied from the code.
- Would this test fail if the code were wrong? Pick one test and break the code by hand. If it stays green, the test is decoration.
- Is anything important mocked? Mocks at the boundary are fine; mocks of the code under test are not.
- Did any existing test change? In a feature branch, changed expected values in old tests deserve a question. In a diff,
git diff --stat -- tests/shows it at a glance. - Are the names readable? A failing test name should tell you what broke.
- Is it flaky? Watch for real time, random data, network calls and test order. Ask the agent to run the new tests several times.
Automatic code review helps here too. A second model reading the diff tends to spot a test that asserts nothing. The AI code review post covers how that works.
Common mistakes
- “Write tests for this file” with nothing else. You get the implementation, restated.
- Chasing a coverage number. You get lines executed, not behaviour checked.
- Letting the implementer edit tests. The fastest way to green is a changed assertion.
- Updating snapshots without reading them.
-uturns a regression into the new expected output. - Adding tests for code you are about to delete. Test the behaviour you will keep.
- Leaving flaky tests in. An agent that sees a flaky test fail will happily “fix” something that wasn’t broken. The same applies to fixing CI failures with AI.
Doing it in AgentVera
AgentVera runs coding-agent CLIs side by side per project, so the test-agent and implementer setup takes a couple of minutes.
- Two agents, one feature. Open a test agent and an implementer in the same project, on the same CLI or on different ones, such as Claude Code for tests and Codex for code. Put them in a separate worktree from the worktree panel if you want the work off your main folder.
- File ownership keeps the implementer out of the tests. In the test agent’s dialog, add
tests/**to its owned areas. While it runs, other agents can’t edit those files. For Claude Code agents the edit is refused before it happens by a PreToolUse hook; Codex and other CLIs get the rule in their instructions. Refused attempts show up on the team timeline. - Agent messaging for disagreements. When the implementer thinks a test is wrong, it can message the test agent directly and wait for the answer, instead of editing the test.
- Verify checks the result. Write what “done” means in your message, for example “all tests in
tests/billingpass and no test file changed”. Verify turns that into criteria, and when the agent’s turn ends a separate verifier runs the tests and marks each criterion with evidence. The verifier can read the repo and run test commands, but it can’t edit files. - Code review reads the tests. Automatic code review checks each turn’s changes with the Claude model you pick and lists findings with severity and file:line, which is a good second look at weak assertions.
- Checkpoints for bad turns. Every turn is snapshotted, so if an agent rewrites half the test suite in a direction you don’t like, you undo that turn and try again. See how to undo Claude Code changes.
Wrapping up
AI makes writing tests cheap, which means the job shifts from typing tests to judging them. Give the agent behaviour instead of code, keep expected values out of the implementation’s reach, use coverage as a map and mutation testing as the score, and don’t let the agent that writes the code also decide what “passing” means. Do that and the speed is real: a test suite that grows quickly and still fails when something breaks.
Read more:
To try AgentVera, start at the home page.