ANAlpesh Nakrani
SolutionsBlogBooksPraiseAboutWork with me
Back to the blog
Blog/Jul 3, 2026 · 11 min

Testing AI-Generated Code: Why the Model Can't Grade Itself

Testing AI-generated code means treating the model as an untrusted contributor: run deterministic checks first, then tests it never gets to write alone.

Testing AI-generated code means treating the model as an untrusted contributor, not a pair programmer. Run deterministic checks first: lint, type check, and a security scan. Then unit and integration tests that verify behavior, not branch count. Then property-based or mutation tests to catch what the model never considered, and never let it write the tests that are supposed to catch its own mistakes.

Picture the version of this that plays out weekly now: an agent implements a feature, then in the same session writes a full test suite for it, and every test passes green. The pull request looks done. It looks tested. The suite validated the agent's own interpretation of the spec, not the spec itself, and that gap survives review because a passing suite reads like proof.

A closed verification loop does not fail loudly. It passes green, right up until a case the model never considered reaches production.

Key takeaways

If you read nothing else, read these.

  • The model that wrote the code should not write the tests that grade it. A closed loop inherits the same blind spots twice and still passes green.
  • Run deterministic checks before a single test runs. Lint, type checking, and a security scanner catch the cheapest bugs for the lowest cost, before anyone asks whether the code works.
  • Coverage is not the goal. Behavior is. A generated test suite can hit every branch and still miss the one user journey that matters.
  • Property-based and mutation testing catch what example-based tests miss by generating inputs no one thought to write, and by checking whether your suite would even notice a real bug.
  • Heavier verification is a real cost, not a free upgrade. A 2025 study found experienced developers about 19% slower with AI assistance despite expecting a speedup, and the extra scrutiny here adds more of that overhead on purpose.

Why testing AI-generated code needs a different posture

A human teammate's code carries tells. A rushed function, a TODO comment, a variable named quickFix. Those tells tell you where to look first. AI-generated code does not hedge that way. It arrives complete, consistently formatted, and confident whether the logic underneath is sound or not.

That confidence is the actual risk. A reviewer trained to read for tone and structure will pass code that looks finished, because looking finished is the only signal a read-through gives. Testing AI-generated code has to replace that read with something that produces a pass or fail, not an impression: run it, check it against a source of truth the model did not write, and treat "it compiles" as the floor, not the finish line.

Rule one: the model that wrote the code doesn't grade it

If the same session that generated the implementation also generates the tests, both inherit the same assumptions about what the code is supposed to do. A test written to confirm the model's own interpretation of the spec passes every time the implementation matches that interpretation, which is exactly the case where a bug hides.

This is not unique to AI. Developers have always been poor judges of their own code fresh out of the editor. What is different is the scale and the confidence: an agent can generate an implementation and a full test suite for it in the same minute, and the suite looks thorough because it is thorough, against the wrong target.

The fix is structural, not aspirational. Route implementation and test generation through separate contexts: a different session, a different prompt with its own read of the spec, or a human writing the test intent before the model writes a line of the implementation. An ICLR 2024 study of Code Llama, GPT-3.5, and GPT-4 found that letting a model debug its own generated code, "self-repair," produced only modest and inconsistent gains once you priced in the extra compute. Feedback from a stronger source, closer to human-quality, produced a bigger jump than the model critiquing itself ever did.

Separating who writes the code from who writes the tests is a process decision, not a tooling one, and it is the kind of discipline a delivery team either has by default or bolts on after an incident. It is the default ViitorCloud's product engineering team builds around when a client hands us an agent-heavy codebase.

Layer 1: deterministic checks catch the cheapest bugs first

Before a single test runs, three checks cost almost nothing and catch a meaningful share of what AI-generated code gets wrong: a linter, a type checker, and a static security scanner. None of them understand what the code is supposed to do. All three catch real defects for the price of a CI step.

# the cheap layer, before any test runs
$ eslint .
$ tsc --noEmit
$ semgrep --config auto .
# catches unused vars, type mismatches, known-bad patterns
# catches nothing about whether the code does the right thing

An agent that hallucinates an argument, returns the wrong type, or copies an insecure pattern from its training data gets caught here, before it costs anyone a debugging session. This layer is deterministic on purpose: same input, same output, no judgment call. Run it on every diff, gate the merge on it, and move on. It is the floor, not the verification.

Layer 2: unit and integration tests for AI-generated code

Coverage is not the goal. Behavior is. A generated test suite can hit every branch in a function and still miss the one thing a customer needs from the feature, because coverage measures which lines executed, not whether the right outcome happened.

Claude Code's own documented workflow for tests treats this as a sequence, not a single pass: find the code with no tests, generate scaffolding, then explicitly ask the model to add edge case and boundary condition tests a human reviewer might miss, then run the suite and verify it actually fails when the logic is wrong (Claude Code common workflows). That last step matters more than it sounds. A test that always passes, even against broken code, is not a test.

Anchor the suite to something the model did not write: the product spec, a user journey, a contract with another service, an invariant that must always hold, the kind of rule that reads like "a finalized invoice total cannot change without a credit note." Those sources catch what a self-referential test cannot, because they come from outside the context that produced the implementation.

Layer 3: property-based and mutation testing catch what examples miss

Example-based tests check specific inputs the author, human or model, thought to write down. Property-based testing flips that: you state a rule that should always hold, and a tool like Hypothesis or fast-check generates hundreds of inputs trying to break it, including the edge cases nobody thought to type by hand.

Mutation testing answers a different question: would your suite even notice if the code were wrong? A mutation tool changes an operator, a boundary, or a comparison in the implementation, then reruns the suite. A test suite that still passes against broken code is not protecting you. It is decoration.

Picture a refund function an agent generated alongside its own unit tests, all green. Flip the comparison operator that guards against a negative refund amount, rerun the suite, and it still passes. The tests checked that refunds worked. Nothing checked that refunds could not go negative, because the model never imagined a customer or an attacker would try.

What human review still has to catch

No layer of automated testing catches everything, and some failure modes are not testable in the conventional sense. A hallucinated API call to a method that does not exist will often fail loudly at build time, but a hallucinated business rule, one the model invented because it sounded plausible, passes every test built on the same invented premise.

Human review earns its place on three things tests structurally cannot verify: intent drift (does this still solve the problem the ticket described, or a nearby problem the model found more tractable), architectural fit (does this change belong here, or did the agent bolt a pattern onto a codebase that already solved this differently), and security boundaries, where "it passed the test I wrote" is not the bar. I go deeper on what a reviewer should be doing differently once the author is a model in my piece on AI code review.

Building the verification loop into your AI coding workflow

None of this works as a one-time checklist. It has to be wired into the loop the agent already runs: explore, plan, implement, verify. Claude Code, Copilot, and Cursor all support this pattern now, running lint and type checks automatically, then handing the diff to a test command before declaring the task done.

The verification layer should get stricter as the agent gets more autonomy, not looser. A human reviewing every single-line change does not scale and does not need to. An agent running unsupervised for an hour against a real codebase needs the full stack: deterministic checks, behavior tests anchored outside its own context, and property or mutation tests on anything that touches money, auth, or data integrity. I lay out the fuller shift in autonomy, and where it should expand or stay capped, in AI-native vs AI-assisted and in my field guide to the AI-native SDLC.

The trade-off: when this much testing isn't worth it

Name the cost honestly. Heavier verification, deterministic scanning, human-reviewed test intent, property-based and mutation testing on top of unit tests, is exactly the kind of overhead that can make a team slower, not faster, even when it is the right call. A 2025 randomized study had sixteen experienced open-source developers complete 246 real issues with AI assistance. They took about 19% longer than without it, despite expecting a 24% speedup, and still believed afterward the AI had sped them up by roughly 20% (METR, 2025). Code quality was similar either way. The slowdown was not a quality trade-off. It was a false sense of velocity.

Heavier verification is a bet, not a free upgrade, and it is not always worth making on low-stakes code.

This is not an argument against testing AI-generated code. It is an argument for sizing the verification to the stakes. A script that reformats a CSV once does not need a mutation-tested property suite. A function that touches a refund, an auth boundary, or a customer-facing promise does, and the revenue math backs that up: a property test suite costs hours, and a refund bug that shipped because a green suite lied to you costs the incident, the refund, and the trust it takes to earn back.

Developers using an AI assistant also tend to trust the result more than they should. A Stanford study found participants using an OpenAI Codex-based assistant wrote measurably less secure code than a control group with no AI access, across several languages and tasks, while rating their own code as more secure than the group that wrote it unassisted (Perry et al., Stanford CCS 2023). Confidence and correctness moved in opposite directions. That gap is the whole argument for the layers above: build a loop that does not depend on how confident anyone, human or model, feels about the result.

Frequently asked questions

Can I trust the tests an AI wrote for the same code it just generated?

Not as your only signal. A test written by the same session that wrote the implementation checks whether the code matches the model's interpretation of the task, not whether that interpretation was correct. Anchor at least part of the suite to something the model did not generate: the spec, a user journey, or a rule a human wrote down first.

Do I need to write more tests for AI-generated code than for code I wrote myself?

You need different tests, and usually more of the layers, even if the raw count looks similar. Deterministic checks, behavior tests anchored outside the model's own context, and property or mutation testing on anything high-stakes are the layers that catch what a same-session suite structurally cannot.

Is AI-generated code less secure than code written without an AI assistant?

The evidence says it can be, and that developers using an assistant tend to feel more confident about code that is measurably less secure. A Stanford study found this pattern held across languages and tasks (Perry et al., 2023), which is the argument for a security scanner as a deterministic, non-negotiable step rather than a judgment call.

What's the best way to test AI-generated code for bugs unit tests miss?

Property-based testing and mutation testing, layered on top of unit and integration tests, not instead of them. Property tests generate inputs nobody thought to write by hand. Mutation testing checks whether your existing suite would even notice if the implementation were wrong, which a green suite alone never tells you.

Where this leaves you

Testing AI-generated code is not a stricter version of testing code you wrote yourself. It is a different discipline built on one assumption: the author cannot be trusted to grade its own work. Deterministic checks first, behavior tests anchored outside the model's own context second, property and mutation testing on anything that matters third. Size the layers to the stakes, and never let one session write both halves of the loop.

I cover the fuller shift, what changes across the whole development lifecycle once an agent runs the loop end to end, in my book, The AI-Native SDLC.

Share
Next

Keep reading

View all blogs

Ask AI about Testing AI-Generated Code: Why the Model Can't Grade Itself