Short answer

Most AI-generated tests prove only that code runs. Keep a generated test only if it builds, passes repeatedly, and fails when the code is deliberately broken. Mutation testing measures that last part; coverage does not, and Meta's production test generators filter on exactly these steps.

Ask a coding agent for tests and you get tests. Lots of them, neatly named, all green, coverage up twenty points by lunch. Then a real bug ships through code that every one of those tests executed.

The problem isn’t that AI writes bad tests. It’s that the default way we judge tests, “do they pass, and how much do they cover”, says almost nothing about whether they would catch a bug. A test generator optimised for green and coverage will produce tests that are green and cover things. If you want tests that catch bugs, you have to measure catching bugs.

Why passing tests prove so little

A test has two jobs. It must pass when the code is right, and it must fail when the code is wrong. Generated tests are very good at the first job, because the generator usually reads the current code and writes assertions that match what it does. That is also why they are often bad at the second. If the code has a bug, a test written by reading that code will tend to assert the bug.

Common shapes of tests that only pass:

  • The test calls the function and asserts that no exception was thrown.
  • It asserts that the result is not null, or has the right type, but not the right value.
  • It mocks so much that the code under test barely runs.
  • It snapshots whatever the current output is, so any change, right or wrong, is just a snapshot to update.
  • It checks the happy path that the developer already tried by hand.

All of these raise coverage. None of them would fail if you flipped a < to <= in the code they cover.

Coverage is not a measure of test quality

This isn’t a new finding about AI. In 2014, Inozemtseva and Holmes generated 31,000 test suites for five large Java systems of up to 724,000 lines and measured how well each suite detected injected faults. Once suite size was controlled for, the correlation between coverage and fault detection was low to moderate, and stronger forms of coverage didn’t help. Their conclusion was that coverage is useful for finding untested code but should not be used as a quality target.

AI test generation makes that warning urgent, because coverage is now nearly free. An agent can hit 90% in an afternoon. The question is what the remaining measure should be.

Here is a model of why it matters. Two suites cover the same lines. One asserts real behaviour, the other mostly asserts that things ran.

Same coverage, different bug-catching power

Two hypothetical suites for one module

Suite A line coverageSuite A line coverage: 90%90%Suite A mutants killedSuite A mutants killed: 35%35%Suite B line coverageSuite B line coverage: 90%90%Suite B mutants killedSuite B mutants killed: 80%80%Suite A line coverageSuite A line coverage: 90%90%Suite A mutants killedSuite A mutants killed: 35%35%Suite B line coverageSuite B line coverage: 90%90%Suite B mutants killedSuite B mutants killed: 80%80%
Model, not measured data. Illustrative, not measured. Suite A asserts mostly that code ran without errors; Suite B asserts specific values and edge cases. Mutants killed is the share of deliberately broken copies of the code that at least one test catches.
Data as a table
Value
Suite A line coverage90%
Suite A mutants killed35%
Suite B line coverage90%
Suite B mutants killed80%

Mutation testing: breaking the code on purpose

Mutation testing measures the second job directly. A tool makes small deliberate changes to the code (a mutant): flip a comparison, delete a line, return a constant. Then it runs your tests. If a test fails, the mutant is “killed”. If every test still passes, the suite would not have noticed that bug.

The share of mutants killed is the mutation score, and it is a much more honest number than coverage. Its weakness has always been cost and noise: many mutants, slow runs, and “equivalent” mutants that don’t change behaviour at all.

Google solved enough of that to run it in daily work. Its mutation system surfaces mutants during code review, only on changed lines, and its 2018 paper reported it in use by 6,000 engineers and processing about 30% of all diffs with statement coverage calculated. The practical lesson from Google is to run mutation testing on the diff, not the whole codebase, and to show only the mutants a reviewer can act on.

How Meta filters LLM-generated tests

Meta has published two papers on generating tests with LLMs in production, and both are useful mainly for what they throw away.

TestGen-LLM generated tests for Instagram and only kept those that passed a series of filters: the test must build, must pass reliably across repeated runs, and must add coverage that the existing suite didn’t have. Most candidates didn’t make it.

How many LLM-generated tests survived each filter

Meta TestGen-LLM on Instagram Reels and Stories

Built correctlyBuilt correctly: 75%75%Passed reliablyPassed reliably: 57%57%Increased coverageIncreased coverage: 25%25%Built correctlyBuilt correctly: 75%75%Passed reliablyPassed reliably: 57%57%Increased coverageIncreased coverage: 25%25%
Source: Alshahwan et al., Automated Unit Test Improvement using LLMs at Meta, FSE 2024.
Data as a table
Value
Built correctly75%
Passed reliably57%
Increased coverage25%

A quarter of the generated tests added anything. The rest either failed to build, were flaky, or duplicated what existing tests already did. Even so, the filtered output was worth having: in Meta’s test-a-thons, the tool improved 11.5% of the classes it was applied to, and engineers accepted 73% of its recommendations.

The follow-up, ACH, went further and used mutants as the target. Instead of “add coverage”, the goal was “write a test that kills this specific mutant”, where the mutants represent faults Meta cares about, such as privacy regressions. Applied to 10,795 Android Kotlin classes, it generated 9,095 mutants and 571 tests. In Messenger and WhatsApp test-a-thons, engineers accepted 73% of those tests and judged 36% of them relevant to privacy. That is the shift worth copying: define the bug first, then ask the model for a test that catches it.

A pipeline for AI tests that catch bugs

This is the process I recommend, built from the two Meta papers and Google’s approach.

Keep a generated test only if it earns its place

  • Agent
  • Automated check
Generatetestsfrom specand diffBuild andrundiscardwhat failsRun fivetimesdiscardwhatflakesMutatethe codeon changedlines onlyKeep thekillerstests thatfail on amutantReviewermodelreadsassertsagainstspecsurviving mutantsGenerate testsfrom spec and diffBuild and rundiscard what failsRun five timesdiscard what flakesMutate the codeon changed lines onlyKeep the killerstests that fail on a mutantReviewer modelreads asserts against spec
Illustrative diagram, not measured data.
Steps as text
StepWhat happens
1. Generate testsAgent — from spec and diff
2. Build and runAutomated check — discard what fails
3. Run five timesAutomated check — discard what flakes
4. Mutate the codeAutomated check — on changed lines only
5. Keep the killersAutomated check — tests that fail on a mutant
6. Reviewer modelAgent — reads asserts against spec
Loopsurviving mutants: back from step 4 to step 1

Steps in practice:

  1. Generate from intent, not only from code. Give the model the acceptance criteria and the diff. Tests written only from the implementation tend to assert its bugs.
  2. Build and run. Drop anything that doesn’t compile or fails on correct code.
  3. Repeat runs. A test that passes four times out of five is worse than no test, because people learn to ignore it.
  4. Mutate the changed lines. Use a mutation tool for your stack (PIT for Java, Stryker for JavaScript and C#, mutmut for Python, Muter for Swift) and restrict it to the diff so it runs in minutes.
  5. Keep tests that kill mutants. A new test that kills no mutant the old suite didn’t already kill adds maintenance without adding protection.
  6. Feed survivors back. A surviving mutant is a precise instruction: “write a test that fails if this line returns a constant”. That is the ACH idea, and models are good at it.
  7. Have a different model read the assertions against the spec. A test can kill mutants and still assert the wrong behaviour, if the code itself is wrong.

One more rule: when an agent is writing both code and tests, its tests need extra suspicion. ImpossibleBench found that when Claude models cheated on impossible tasks, more than 79% of the time they did it by editing the tests. Tests that define done should be written or frozen before implementation, as I describe in acceptance criteria an agent can’t talk its way around, and test edits should be visible to the reviewer, as in evidence over claims.

Good and bad uses of AI for automated QA

Bad: asking for “tests for this file” with a coverage target, accepting everything that passes, and counting the coverage gain as progress. You get a slower suite, a false sense of safety and tests that will be updated, not heeded, when behaviour changes.

Bad: letting the implementing agent update failing tests to match its new code without anyone seeing the change.

Good: tests generated from criteria, filtered by build, stability and mutants killed, read by a reviewer that isn’t the writer.

Good: using models for the tedious parts of QA where they shine, such as generating edge-case inputs, writing fixtures, turning a bug report into a failing reproduction, and explaining why a mutant survived.

I think this is one of the places where automation is furthest ahead of practice. The tools to measure whether a test catches bugs have existed for decades, they were just too slow and noisy to run on every change. A model that writes a test for each surviving mutant removes most of that cost. A team that wires this up gets a test suite that improves on every pull request without anyone writing tests by hand, and that is a real step towards code review with fewer people in it.

Questions

Can AI write good unit tests?

Yes, if you filter them. Meta’s TestGen-LLM kept about a quarter of generated tests after checking that they built, passed reliably and added coverage, and engineers accepted most of what survived.

What is mutation testing?

It is a way to measure whether tests catch bugs by making small deliberate changes to the code and checking whether any test fails. The share of changes caught is the mutation score, a stronger signal of test quality than coverage.

Is code coverage a good measure of AI-generated tests?

No. Coverage shows which code ran during tests, not whether the tests would fail on a bug, and research on large Java systems found only a low to moderate link between coverage and fault detection once suite size is controlled for.

How do you stop AI from writing tests that just pass?

Generate tests from the specification as well as the code, discard tests that don’t fail on a deliberately broken version of the code, and have a different model check the assertions against the expected behaviour.