Skip to content
Back to all field notes verify · 9 min

Testing AI-generated code: what changes and what doesn't

You sit down to test code you didn't write, and the tests keep agreeing with it. They should. You wrote them after reading the code. The intent was never in the room.

field note 9 min

You generate a module. It works when you click through it. Then you sit down to add tests, because you know you should, and something feels slightly wrong. Every test you write passes on the first run. Not after a fix. On the first run. You tell yourself the code was just good. It wasn’t. You wrote the tests after reading the code, so the tests encode what the code does, not what it was supposed to do.

That is the one thing that really changes when you test AI-generated code. Almost everything else about testing is the same as it always was, and the mistakes come from believing the opposite.

The intent was never in the room

A test is a claim about intent. It says “this input should produce this behavior” and it fails when reality disagrees. That only works when the intent exists somewhere independent of the code. When you write the code yourself, the intent lives in your head while you type, and the test you write next is a second expression of the same idea. The redundancy is the point. Two witnesses to the same intent, and if they disagree, one of them is wrong.

With generated code, the second witness is missing. The model produced the most probable implementation of your prompt. If you then read that implementation and write tests against what you see, you have one witness twice. The code says X, the test asserts X, they agree, green. You have proven that the code does what the code does. You have proven nothing about what you wanted.

This is why testing generated code the naive way feels so smooth and buys so little. The friction that used to catch bugs, the moment where your test and your memory of the intent scrape against each other, is gone. Nobody in the room remembers what the feature was for. The model never knew. You outsourced the writing and the intent went with it.

What actually changes

Three things move, and they are worth naming precisely because everything else stays put.

The source of truth moves earlier. When you write code, the code can serve as its own rough record of intent because you held the intent while writing it. When an agent writes code, the intent has to be captured before generation, or it evaporates. The acceptance criteria, the behavior you agreed on, the examples of correct and incorrect output, all of that has to exist as an artifact the test can point back to. Not in the chat log. In something durable.

The order of writing moves. If you write the test after reading the generated code, the code contaminates the test. So the test, or at least the behavior it encodes, has to come from the specification, not from the diff. You describe what “correct” means, then let the agent generate, then check the generation against a test that never saw the implementation. Same discipline as test-first development, for a sharper reason: not to shape your design, but to keep the code from writing its own exam.

The volume moves. A single person could generate a week of code in an afternoon. Each of those changes needs its behavior pinned. You are no longer testing the code you slowly wrote, you are testing a flood of code you skimmed. Coverage that used to accumulate as a side effect of working now has to be a deliberate gate, because the working part got faster and the understanding part did not.

What doesn’t change, no matter what anyone sells you

A test still only proves what it exercises. A green suite over generated code means the same thing it always meant, that the paths you wrote assertions for behaved as asserted, and nothing about the paths you didn’t. The model does not make your untested branches safe. If anything it writes more branches, faster, so the gap between “tests pass” and “the feature is correct” gets wider, not narrower.

Coverage still lies about the thing you care about. Line coverage tells you a line executed, not that an assertion would have caught it breaking. You can run every line of a generated module and assert almost nothing, and the number looks great. Martin Fowler’s warning that test coverage is a useful signal and a terrible target applies with full force here, because agents are very good at producing tests that raise the number and check nothing real. Ask one to “add tests” and watch how many of them assert that a function returns without throwing.

The pyramid still holds. A pile of end-to-end tests over generated code is as slow and flaky as it ever was, and a base of fast unit tests that actually pin behavior is still what lets you change things without fear. The practical test pyramid did not get repealed because a machine is typing now. And self-testing code, the property that the system can prove itself correct on demand, is more valuable when you didn’t write the system, not less. It is the only thing standing between you and a codebase you have to trust on faith.

The test the agent writes for its own code

There is a specific failure mode worth calling out because it is now the default. You ask an agent to implement a feature and to write tests for it. It does both, in the same turn, from the same context. The tests pass. You feel covered.

You are not covered. You have a system where the same process wrote the implementation and the exam, holding one idea of the behavior in its head, and graded itself. If the model misunderstood the requirement, it misunderstood it consistently, and the tests encode the misunderstanding as the expected result. The green is real. The correctness is not. This is the near cousin of the agent that reports “tests pass” when nothing ran, except worse, because here the tests genuinely ran and genuinely passed, and they are still worthless as evidence of correctness.

The fix is separation. The behavior gets specified by you, or generated in a separate step from a different framing, and pinned before the implementation exists. Then the implementation is generated against it. The exam has to predate the answer, or it is not an exam.

How to keep tests true with generated code

Write the assertions from the specification, not the diff. If you find yourself opening the generated file to decide what to assert, stop. You are about to ratify the code instead of testing it. Go back to what the feature was supposed to do.

Pin behavior at the boundary, not the internals. Generated code churns. If your tests are welded to the private structure the agent happened to produce, they break on every regeneration and you learn to ignore them. Test the observable contract, the input and the output and the side effect, so the tests survive the agent rewriting the middle.

Keep at least one test the agent never saw. When you generate the implementation and the tests together, add your own case afterward, from the spec, by hand. One true witness is enough to catch a whole class of confident nonsense.

Treat coverage as a floor with teeth, not a trophy. A number nobody can drop below is a gate. A number on a dashboard is decoration. If the gate can be satisfied by tests that assert nothing, the gate is decoration too.

None of this is new testing theory. It is old testing theory applied to a situation where the intent left the building. The whole discipline of verifying AI-generated code rests on this: a test is only evidence when it was written to defend a behavior that exists outside the code it tests. The moment the test starts describing the implementation instead of the requirement, you are back to trusting a machine’s confident sentence, dressed up in green.

Where PaellaDoc fits

The reason tests drift into ratifying the code is that the intent has nowhere to live. The chat scrolls away, the ticket says “add search,” and six weeks later nobody can say what “correct search” meant. PaellaDoc keeps the acceptance criteria, the decisions, and the behavior as durable artifacts next to the code, so the test has something to point back to that the implementation never touched. The agent writes the code. The criteria you agreed on beforehand write the exam. When those two disagree, you find out before it ships, which is the only moment finding out is cheap.

Frequently asked questions

Is testing AI-generated code different from testing code you wrote?

One thing changes: the intent was never in anyone’s head. When you write code, the intent lives in your mind and the test is a second, independent expression of it. With generated code that second witness is missing, so if you write tests after reading the implementation, they encode what the code does instead of what it was supposed to do. Almost everything else about testing stays exactly the same.

Why do my tests always pass on AI-generated code?

Because you wrote them after reading the code, so they describe the implementation rather than the requirement. The code says X, the test asserts X, they agree, green. You have proven the code does what the code does and nothing about what you wanted. The fix is to write assertions from the specification, not the diff, so the test is defending a behavior that exists outside the code it checks.

Should the same AI write the code and its tests?

Not in the same step from the same context. If the model misunderstood the requirement, it misunderstands it consistently, and the tests encode that misunderstanding as the expected result. The green is real, the correctness is not. Separate them: specify the behavior yourself and pin it before the implementation exists, then generate against it. Keep at least one test the agent never saw. The exam has to predate the answer.

Does high test coverage mean AI-generated code is correct?

No. Coverage tells you a line executed, not that an assertion would have caught it breaking. You can run every line of a generated module and assert almost nothing while the number looks great, and agents are very good at producing tests that raise coverage and check nothing real. Treat coverage as a floor with teeth, a gate nobody can drop below, not a trophy on a dashboard. If tests that assert nothing satisfy the gate, the gate is decoration.