# Write unit tests with AI: a practical guide

URL: https://agentvera.dev/en/blog/write-unit-tests-with-ai/ · Published: 2026-10-25

How to write unit tests with AI agents that actually catch bugs: characterization tests, coverage gaps, mutation testing, and one agent testing while another builds.

**In short:**

- An AI agent writes tests fast, but a test that only mirrors the code it reads proves nothing; ask for behaviour from the spec, not from the implementation.
- Use coverage to find untested code and mutation testing to find weak tests: a test suite that lets a flipped condition survive is not protecting you.
- Splitting the work works well: one agent writes the tests, another writes the code, and the implementer is not allowed to edit the tests to make them pass.
- In AgentVera, file ownership keeps the implementer out of the test folder, Verify checks the result against your criteria, and checkpoints undo a bad turn.

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 `-u` whenever 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 `applyDiscount` in `src/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_codes` are 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](/en/blog/claude-md-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](/en/blog/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](/en/blog/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:

1. Run coverage and pick the modules that matter: billing, auth, data migrations, not generated code.
2. Give the agent the uncovered lines and ask what behaviour they represent.
3. Have it write tests for that behaviour, with expected values from the rules.
4. 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 run `npx stryker run`.
- **Python:** mutmut. Run `mutmut run`, then `mutmut browse` to 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-mutants` and run with `cargo 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 change `discount.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:

1. **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.
2. **Implementer agent.** Gets the same spec plus the failing tests and writes the code until they pass. It may not edit anything in `tests/`.
3. **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](/en/glossary/git-worktree/) per agent is the usual answer; see [git worktrees for parallel agents](/en/blog/git-worktree-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](/en/blog/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.** `-u` turns 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](/en/blog/auto-fix-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/billing` pass 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](/en/blog/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:

- [Verify](/en/features/verify/)
- [File ownership](/en/features/file-ownership/)
- [Agent messaging](/en/features/agent-messaging/)
- [Code review](/en/features/code-review/)
- [Checkpoints and replay](/en/features/checkpoints-and-replay/)
- [Parallel coding agents](/en/blog/parallel-coding-agents/)

To try AgentVera, start at the [home page](/en/).

## Frequently asked questions

**Can AI write good unit tests?** Yes, if you steer it. Agents are good at setup, fixtures, edge-case lists and boilerplate. They are weak when they derive the expected values from the code under test, because then the test only repeats the implementation. Give them the expected behaviour and check the tests with mutation testing.

**What is a tautological test?** A test that passes by construction: it computes the expected value the same way the code does, mocks the very thing it should check, or asserts only that a function was called. It stays green even when the logic is wrong.

**Is 100% coverage a good goal for AI-written tests?** No. Coverage tells you which lines ran, not whether anything was checked. Use it to find untested areas, then judge the tests with mutation testing or a careful review.

**Should the same agent write the tests and the code?** For small changes it is fine. For important logic, splitting helps: one agent writes tests from the spec, another implements, and the implementer may not change the tests. That stops the easiest cheat, editing the test until it passes.

**Which agents can I use for this in AgentVera?** Any CLI agent AgentVera runs, such as Claude Code, Codex or Gemini CLI. File ownership blocks edits up front for Claude Code agents; Codex and other CLIs get the ownership rules in their instructions.

---

**About AgentVera.** AgentVera is a desktop app for macOS, Windows and Linux, built by Solviera Teknoloji. It runs 18 AI coding agents (Claude Code, Codex, Gemini CLI, Copilot, Cursor, Antigravity, opencode, Hermes, Qwen Code, Kimi Code, Kiro, Amp, Droid, Goose, Vibe, Crush, Aider, Cline) side by side, per project, in real terminals with the user's own CLI installs and logins. The free plan has unlimited agents. Download: https://agentvera.dev/en/download/ · Pricing: https://agentvera.dev/en/pricing/
