Checks that keep working at high speed

Test strategy when the AI writes the tests too

When the same system writes the implementation and the tests, a passing suite can mean the code agrees with itself. That is not evidence. This page covers how to keep tests independent of the code they check, which tests are worth generating and which are not, and the three properties a suite needs before you can let a passing result decide a merge.

Published September 1, 2026. Editorial.

Key takeaways

  • A test that has never failed has never been tested. Prove each important test can fail before you trust it.
  • Independence matters most: the behaviour a test asserts should come from the specification, not from the implementation.
  • Generate the many simple checks. Write the few detailed cases about money, permissions and data loss by hand or from the spec.
  • Volume is not confidence. A thousand generated assertions about internal structure protect less than twenty about observable behaviour.

Every team that adopts AI-native development eventually notices the same thing. The test suite grew four times, the coverage number went up, and the number of bugs reaching customers did not go down. Usually it went up, at the same rate as the change volume.

The reason is that generated tests default to describing the implementation rather than the requirement. Asked to write tests for a function, a model reads the function and asserts what it does. Every assertion passes on the first run, which feels like success and is actually a tautology: the code does what the code does.

The independence problem

A test is useful when it encodes an expectation formed independently of the implementation. That is the whole mechanism. If the expectation came from reading the code, the test cannot detect a case where the code is wrong, because wrongness is exactly agreement with an incorrect implementation.

This was already true when humans wrote tests after the fact, which is why test-first practice existed long before language models. What changed is scale. A human writing tests after the code produced a few after-the-fact assertions. An agent produces two hundred, and the suite looks comprehensive.

Three practical rules follow.

Derive tests from the specification, not the diff. The input to test generation should be the acceptance criteria, in the words that existed before the implementation, in a session that is not looking at the implementation. This costs a little more setup and changes what the tests are for.

Prove the test can fail. Before trusting an important test, break the behaviour it is supposed to protect and confirm it fails. A test that has never failed is an assertion with no demonstrated relationship to the code. This is the single cheapest quality practice available in an AI-native team, and almost nobody does it consistently.

Keep the checks that decide a merge separate from the code they check. If the session implementing a feature can also relax the criteria that judge it, the standard is advisory. This is the same protection principle as the quality gates that should block a merge.

What to generate and what to write

AI test writing is good at some of this and bad at other parts. The split we use:

Generate: input permutations, boundary values, malformed input, unusual encodings, empty and enormous collections, concurrency ordering variations. This is the tedious broad coverage humans skip out of boredom, and models never get bored of it and find creative cases. It finds real bugs.

Write from the specification: the cases about money moving, permissions being enforced, data being deleted, and anything with a regulatory consequence. Not because a model cannot express these, but because the expected behaviour has to come from a person who understands what the business needs, phrased before the implementation exists.

Do not bother with: assertions about internal structure, mock-heavy tests that verify one function called another, and snapshot tests over anything a model might reasonably change. These break constantly under AI-driven refactoring and teach the team to update the expectation rather than investigate the difference. A suite that trains people to accept new output without checking it is worse than a smaller one.

The three properties a suite needs

Before you let a passing suite decide a merge, the suite needs to deserve that trust.

It fails for real reasons. Tests that fail at random make the result meaningless. In a high-volume pipeline a test that fails at random once in fifty runs becomes a daily interruption, and the team learns that a failure may mean nothing. Take such tests out, fix them, and put them back rather than tolerate them.

It fails fast and locally. A failure should name the behaviour that broke, not just the assertion that failed. When a change from an agent breaks something, the useful output is which requirement is now violated, because that is what the next prompt needs.

It covers the observable behaviour, not the structure of the code. The test suite is a description of what the product must do. Anything asserting how it is done is a constraint on refactoring, and in a codebase being continuously refactored by agents that constraint gets removed rather than respected.

Coverage is a trend, not a target

Coverage percentage is the most commonly misused number in this area. It measures which lines ran, not whether anything was checked, and an agent asked to increase it will do so by adding assertions to code paths rather than expectations to behaviours.

Use it in one direction only: a sudden drop is worth investigating, because it usually means a body of new code arrived with no tests. As a target it produces exactly the tests that make a suite larger without making it better at catching bugs. The deeper argument about what to measure instead is in metrics for AI code quality and in our post on how many tests does AI-generated code need.

Why this matters more than it used to

Stack Overflow's 2025 survey found the top developer frustration with AI tools is output that is almost right but not quite, reported by 66 percent, with debugging generated code taking more time than expected second at 45.2 percent [1]. Both describe errors that a casual read does not catch. A test suite is the mechanism that catches almost right, but only if the expectations in it came from somewhere other than the code being tested.

Veracode's measurement of generated code security is a clearer version of the same point: syntax correctness is high while security correctness is much lower [2]. Code that parses, runs, and looks right is the normal case. The test suite exists precisely for the gap between looking right and being right, which is why filling it with assertions derived from the implementation defeats the purpose.

A workable structure

For most product teams the structure that works is three layers, in decreasing volume.

A wide layer of fast generated checks over pure logic and input handling, run on every change, cheap to keep. A narrower layer of behaviour tests written from the specification, covering the flows that matter to the business, protected from casual modification. And a thin layer of end-to-end checks over the two or three user flows whose failure would be an incident, kept deliberately small because they are slow and the most likely to fail at random.

The middle layer does most of the useful work, and it is the one teams under-invest in because it cannot be produced in bulk. That is not an argument against generation. It is an argument for spending the time AI saved on the tests that had to be thought about.

What to do with a suite you already do not trust

Most teams reading this are not starting from nothing. They have four thousand generated tests, a passing pipeline, and no confidence in either.

Deleting the suite is tempting and wrong, because some of it is protecting something and you cannot tell which parts without looking. The approach that works is sorting the tests in three passes, and it is a weekend of work rather than a project.

First, find the tests that cannot fail. Run the suite against a deliberately broken build: comment out a branch of important logic, invert a condition in a permission check, make a repository method return an empty result. Anything that still passes is not testing what its name claims. That set is usually larger than anyone expects, and it is the first thing to fix or delete.

Second, find the tests that fail for the wrong reasons. Anything that breaks whenever an implementation detail changes is an extra cost on every future refactor, and in a codebase being continuously refactored it will be deleted eventually anyway, usually in a hurry and without judgment. Better to remove it now, deliberately, than to have it removed by whoever is under time pressure later.

Third, write the missing middle layer. List the flows whose failure would be an incident, check which of them have a test derived from a requirement rather than from the code, and write the ones that are missing from the specification. There will not be many, and they are the tests that will actually stop something.

At the end you usually have fewer tests, a faster suite, and a result you can use as a gate. That combination is worth more than any coverage figure.

Where this connects

The full discipline of writing checks from a specification, running them in continuous integration, and knowing when they give false results is the eval-driven development guide, which is the companion to this one. What tests gate is covered in the quality gates that should block a merge, and what to do when they pass and the change still breaks in production is incident response for agent-written code. The main guide sets out the wider delivery model.

Common questions

Can AI-generated tests be trusted?

Only when the expectation inside them came from somewhere other than the implementation. Asked to test a function, a model reads it and asserts what it does, so every assertion passes on the first run and none of them can detect that the behaviour is wrong. Generate from the acceptance criteria instead, in a session that is not looking at the code.

How do we know a generated test actually checks anything?

Break the behaviour it protects and confirm the test fails. A test that has never failed is an assertion with no demonstrated relationship to the code. This takes a minute per important test and is the cheapest quality practice available in an AI-native team, which is why it is worth doing before the suite is trusted to gate merges.

Which tests should be generated and which should be written by hand?

Generate the wide, tedious coverage: input permutations, boundary values, malformed input, empty and huge collections, ordering variations. Write from the specification the cases about money moving, permissions being enforced, data being deleted, and anything with a regulatory consequence, because those expectations must come from a person who knows what the business requires.

Why did our bug count stay flat after the test suite grew four times?

Because volume is not evidence. A large suite of assertions derived from the implementation confirms the code agrees with itself. Twenty tests describing observable behaviour, written from requirements, protect more than a thousand that describe internal structure, and the smaller suite also survives refactoring instead of being rewritten by it.

Are snapshot tests a good idea when agents refactor the code?

Usually not. Snapshots break constantly under AI-driven refactoring and train the team to accept the new output rather than investigate the difference. Once accepting the diff becomes routine, the test has inverted: it now records whatever the code produces instead of what the product is supposed to do.

Should code coverage be a target in an AI-native team?

No. Use it as a trend in one direction: a sudden drop is worth investigating because it usually means new code arrived untested. As a target it invites an agent to add assertions to code paths rather than expectations to behaviours, which makes the suite larger without making it better at catching bugs.

What structure should a test suite have when most code is generated?

Three layers. A wide, fast layer of generated checks over pure logic and input handling. A narrower layer of behaviour tests written from the specification, covering the business-critical flows and protected from casual edits. A thin layer of end-to-end checks over the two or three user flows whose failure would be an incident.

How should a test failure be reported so an agent can act on it?

Name the behaviour that broke, not only the assertion that failed. The next prompt needs to know which requirement is now violated. A failure message that says an expected value did not match sends the agent to make the assertion pass, which is a different and often wrong task.

Should the same agent write the code and its tests?

Not in the same pass with the implementation in view. Either derive the tests from the specification first and let the implementation satisfy them, or use a separate session that sees the criteria and not the code. What matters is that the expectation is formed independently, because agreement between code and tests written from that code proves nothing.

Does more testing slow an AI-native team down?

The wrong tests do. Mock-heavy tests asserting internal structure break on every refactor and consume review time without catching defects. Fast checks over observable behaviour speed the team up, because they let changes merge on automated evidence instead of waiting for a person to find time to read the diff.

Start a project