DEV Community

Cover image for AI Coding Assistants Write Tests That Pass, Not Tests That Catch Bugs
AI Explore
AI Explore

Posted on

AI Coding Assistants Write Tests That Pass, Not Tests That Catch Bugs

TL;DR — AI coding assistants generate tests by reading the implementation, so the tests inherit the implementation's blind spots instead of checking against the intended behavior. This produces high coverage numbers with low bug-catching power. The fix is decoupling test generation from implementation access through spec-first prompting, property-based testing, and mutation testing as a reality check.

Ask an AI coding assistant to write tests for a function, and it will almost always produce something that passes on the first try. This feels like a win. It is usually a trap. The assistant read your implementation before it wrote the test, which means the test is not checking your function against the behavior you intended — it's checking your function against itself.

If there's a bug in the implementation, there's a good chance the same bug is now encoded into the test as an assertion. The checkmark turns green. Coverage goes up. Nothing was actually verified.

The coverage trap

Coverage percentage measures which lines executed during a test run. It says nothing about whether the right outcomes were asserted. A test suite can hit 95% coverage and catch zero regressions if every assertion was derived from watching the code run rather than from a specification of what the code should do.

This isn't a hypothetical failure mode. It's the default behavior of how these tools work. When an assistant has read access to a function's body — and in most editor integrations, it does, because that's the whole point of the context window — it conditions its test generation on that body. Give it a function with an off-by-one error in a loop boundary, and it will frequently write a test that asserts the off-by-one output, because that's what it observed when it reasoned through the function, not because it independently derived the correct boundary from the spec.

The tell is subtle: the tests look thorough. They cover the happy path, a null case, maybe an empty-array case. They just don't cover the one case that would have exposed the actual defect, because that case requires knowing what the function should do independent of what it does do — and the model wasn't given that information.

Why this happens, mechanically

Test generation is a function of available context. If the only context is the implementation, the model has no independent signal for correctness. It cannot distinguish "this is the behavior" from "this is the correct behavior" unless something in the prompt — a docstring, a ticket description, an existing contract test, a type signature with real semantic meaning — encodes the spec separately from the code.

This is the same failure mode you see in human engineers who write tests after the fact, except it happens faster and at higher volume. A junior developer writing a test suite for code they just wrote will also tend to assert "what it does" rather than "what it should do," because the implementation is sitting right there, authoritative and convincing, while the original intent has already faded from working memory. AI assistants don't have working memory to fade — they have no independent memory of intent at all unless you explicitly supply it every time.

Where assistants genuinely help with testing

None of this means AI-generated tests are worthless. They're excellent at the mechanical layer of testing, which is a real bottleneck:

  • Boilerplate and scaffolding. Setting up fixtures, mocking dependencies, writing the parametrize decorators for a table of inputs — this is pure plumbing and assistants are fast and reliable at it.

  • Expanding an existing, human-written assertion into edge cases. If you write one correct test by hand that encodes the actual spec, an assistant is good at generating variations — different input shapes, different locales, different sizes — that exercise the same assertion logic.

  • Regression tests for a bug you already found and fixed. Here the spec is unambiguous: the bug report. The assistant isn't guessing at intent; you've handed it the exact failure case and the exact correct output.

  • Translating a spec document into test skeletons. If you paste in an API contract or an RFC-style description before the assistant ever sees the implementation, it's generating from the spec, not from the code — which is the mode you actually want.

The pattern across all four is the same: the assistant performs well when the correctness signal comes from somewhere other than the implementation it's testing. It performs poorly the moment the implementation is the only available source of truth.

What actually catches this

You can't fix this with better prompting alone, because the structural problem is an information asymmetry, not a wording problem. A few concrete practices close the gap:

  • Spec-first generation. Write or paste the expected behavior — a docstring, a contract, a table of input/output pairs — before you let the assistant see the implementation. Generate tests from that, then run them against the code. This is slower per-ticket but produces tests that can actually fail.

  • Property-based testing. Instead of asking for example-based assertions, ask for invariants: idempotency, commutativity, round-trip consistency, monotonicity. Properties are harder to accidentally copy from a buggy implementation because they're abstract constraints rather than concrete observed outputs. An assistant asked "what properties should hold for a sort function" is reasoning about sorting in general, not about your specific buggy comparator.

  • Mutation testing as a trust metric. Line coverage tells you what ran. Mutation testing tells you whether your assertions would notice if the logic changed. Run a mutation testing tool against AI-generated suites specifically — it's the fastest way to discover that a 90%-covered module has a mutation score near zero, meaning the tests exercise the code but don't constrain it.

  • Keep a human or a second, blinder model in the loop for the assertion itself. Let the assistant write the scaffolding and generate input variations. Require a human — or a model prompted only with the spec, never shown the implementation — to write or approve the actual expected-value assertions.

The real shift this implies

The broader lesson is that "AI wrote tests for this" is not a meaningful quality signal by itself. The meaningful question is what the tests were generated from. Tests derived from the implementation are a mirror. Tests derived from an independent spec are a check. Those look identical in a CI dashboard — both are green — and that's exactly the problem. Teams adopting AI-assisted testing at scale need to start tracking provenance of test intent the way they'd track provenance of training data: where did the expected value come from, and was it ever exposed to the thing it's testing. Without that discipline, you're not increasing test coverage. You're increasing the illusion of it, one convincing green checkmark at a time.

Top comments (0)