Skip to main content
Back to Blog
ai code testingai generated codecode review workflowstatic analysisci automation

How to Test AI Generated Code Without Trusting the Tests

Greg Ceccarelli
Greg Ceccarelli
·15 min read

A green test suite can coexist with insecure, hard-to-maintain code. In a comparative evaluation of five language models, Claude Sonnet 4 had a higher unit-test Pass@1 rate than OpenCoder-8B, yet its passing outputs averaged more SonarQube issues per task. The study found no direct correlation between functional performance and overall code quality or security, so test success is a starting signal, not a production verdict (comparative evaluation of language models).

Learning how to test AI-generated code means checking more than whether the output runs. You need independent gates for behavior, security, maintainability, and whether the agent understood the request. Just as important, reviewers need enough context to trace the change back to the prompt and decisions that produced it.

Table of Contents

Why a Passing Test Suite Is Not Enough

A test result is only useful when you can identify what it exercised. For example, a generated endpoint may return the expected response while accepting requests from users who should have no access. If the suite never checks permissions, its green status says nothing about that boundary.

Read the study of functional and code-quality results as a reason to keep evaluation dimensions separate, rather than as a model leaderboard. For a release decision, the useful question is which risks remain unresolved in this particular change. A behavior result cannot clear a security finding, and a clean static-analysis report cannot establish that the agent implemented the requested behavior.

Record the scope of each check alongside its outcome. “Passed” should identify the candidate, environment, and cases exercised. Otherwise, reviewers cannot distinguish evidence about the submitted change from evidence about an earlier attempt.

A diagram explaining why a passing test suite is insufficient for identifying security, maintainability, and behavioral risks.

Make the execution environment repeatable

Before executing generated code, save the prompt and conversation that produced it, including clarifications and decisions that changed the requirements. Record the model version, temperature, dependency versions, and test seed where available. These records let reviewers compare attempts and investigate failures; they do not guarantee that regeneration will produce identical code.

Keep the actual candidate as the replayable artifact.

Run it in an isolated container or sandbox with CPU, memory, filesystem, and network access limited to the task. Use disposable, least-privilege credentials. Keep unreviewed output away from production data and shared infrastructure. Restricting access can make integration checks harder to configure, but granting broad access just to make tests run exposes systems before the code has earned that trust.

Capture source, execution logs, test results, the dependency manifest, and tool versions together. Associate those artifacts with the relevant conversation and revision so a reviewer can reconstruct both what ran and why it was written. A failure report without that context leaves the reviewer guessing which assumption to challenge.

Give each gate its own result

Keep compilation and type-checking, behavior tests, broader correctness checks, security and dependency scans, and human review visible as separate decisions. An unresolved finding should retain its severity and disposition instead of disappearing into an average quality score.

Practical rule: Passing expected-behavior tests proves only that the code passed those tests. It doesn't establish that the code is safe or ready to maintain.

A quick diff review misses assumptions that look reasonable until compared with the conversation. Check whether permissions, failure behavior, and requirement changes have supporting evidence. The release decision should rest on that evidence, with gaps explicitly acknowledged.

Writing Tests That Actually Catch Agent Mistakes

Start with the cheap, deterministic failures. Type-check and compile the output before spending time on broader tests. A compiler can catch mismatched signatures and invalid types, but it can't establish that the behavior matches the product requirement.

A diagram outlining a multi-step testing process to ensure AI agents function correctly and catch software errors.

Test behavior, then test the boundaries

Write unit tests around observable behavior, not the agent's chosen implementation. If a function normalizes input, test the expected output for ordinary values and for malformed or boundary inputs. Avoid tests that merely assert a particular helper was called or a specific internal structure was created. Those can reward code that follows the expected shape without delivering the required behavior.

Then test real interfaces. Integration tests should exercise how modules communicate, including serialization, persistence, external service adapters, and error handling. If the generated code changes an API, add contract tests so callers and providers agree on the boundary.

Keep the core tests beside the code so future maintainers can understand the intended behavior. Put reviewer-controlled hidden cases in a separate suite or CI configuration when exposing every test would encourage an agent to optimize narrowly for visible checks.

Probe what the prompt didn't spell out

Add tests for malformed input, authorization, concurrency, timeouts, and failure recovery. These cases often reveal gaps that a clean happy-path test misses. A benchmark's Pass@k measures whether at least one generated sample passes its supplied tests, not whether it is correct in production; a thorough protocol adds hidden tests, property or invariant checks, static analysis, and human review (guidance on layered testing and benchmark limits).

A test can also pass while failing to test the request itself. For example, if an agent adds a search endpoint, assert that it returns the right records and respects access rules, not merely that the endpoint responds successfully. For teams comparing detection and review approaches, AI Website Detector's code analysis offers a relevant point of reference, but no automated analysis replaces tests tied to your actual requirements.

The goal isn't to make every test hidden. It's to make the test set broad enough that a candidate can't look trustworthy by satisfying the examples most visible to the agent.

Property Based Tests and Mutation as Honest Witnesses

A passing suite earns confidence only when it can expose broken behavior. Property-based testing checks rules across generated inputs; fuzzing exercises malformed or unexpected inputs. Both help with agent-written parsers, validators, normalization helpers, and permission checks, where a small diff can hide a large input space.

Start with an invariant grounded in the prompt and any later clarification. A parser should handle arbitrary input without crashing; normalization should stay stable when repeated; a transformation should preserve the required relationship between input and output. Record which requirement each property tests so a reviewer can trace the assertion back to the conversation rather than accept the agent's interpretation. Then let the tool generate cases beyond the hand-written examples. Fuzzing adds pressure where invalid input is routine, especially parsers and interfaces accepting untrusted data.

The survey of property-based testing results cited earlier reported that 30–32% of solutions that passed ordinary unit tests still failed fundamental correctness properties when property-based tests were applied.

That result makes test-pass rate a starting point for validation, with generated counterexamples providing concrete evidence of what the examples missed.

A chart comparing bug detection rates between unit tests, property-based tests, and mutation testing with mutation score trends.

Mutation testing checks whether deliberate code changes make the suite fail. Tools can alter comparisons, remove conditions, or change return values. Investigate surviving mutations against the intended behavior: an unchanged result may expose a missing assertion, though some mutations do not change observable behavior. Use mutation score to locate weak tests, accounting for whether the mutations resemble plausible defects in that code.

Run deterministic tests on every change and focused property checks around stated invariants. Reserve longer fuzzing and mutation runs for input-heavy or high-risk paths, using parallel jobs or broader CI stages to contain runtime. Save failing inputs, replay seeds, and relevant prompt context with the result. Reviewers then have a reproducible failure and its requirement, rather than a green badge detached from the decision that produced the code.

The Security Gate That Has to Run Before Humans

Security shouldn't depend on whether a reviewer happens to notice a dangerous pattern in a diff. Put automated checks in CI, and treat them as an independent gate after functional tests. A 2021 empirical study tested GitHub Copilot using 89 programming scenarios based on high-risk MITRE CWE categories and generated 1,692 programs. Approximately 40% contained vulnerabilities or exploitable design flaws, and the authors recommended security-aware tooling during both code generation and evaluation (empirical study of AI-generated code vulnerabilities).

A practical security sequence looks like this:

  1. Run static analysis. Use a tool such as Semgrep or CodeQL to flag insecure patterns and risky data flows.
  2. Scan secrets. Check the diff and repository for credentials, tokens, and other sensitive strings before they reach a shared branch.
  3. Check dependencies. Use software-composition analysis to identify known vulnerabilities and review newly added packages.
  4. Add targeted adversarial tests. Probe the failure modes relevant to the code, including injection, improper authentication, unsafe deserialization, and path traversal.
  5. Review the threat model. Ask what inputs are untrusted, what data crosses a boundary, and what privileges the code needs.

For execution checks, run generated code in a restricted code-execution sandbox, not against shared infrastructure. Limit its permissions and network access, and use disposable credentials. A sandbox contains mistakes during evaluation; it doesn't establish that the resulting application is secure.

Escalate authentication, authorization, payments, cryptography, and sensitive data handling to a named human expert. Automated scanners can surface known patterns, but a reviewer still has to assess whether the design and assumptions fit the application. Acceptance rule: passing functional tests isn't validation until the relevant security checks have also passed or a documented human decision has resolved their findings.

Trace Every Change Back to the Prompt and the Decision

A diff shows what changed, not why the agent changed it. If the prompt omitted a product decision or the conversation lost a constraint, a technically tidy implementation can still be wrong. Treat that missing context as a quality defect, not merely a documentation gap.

A 2025 industry report found that 65% of developers using AI for refactoring, and about 60% using it for testing or review, said the assistant missed relevant context. Separate research with industry developers found that participants could produce insecure code while remaining confident in its security (industry report on AI code quality and context). Those findings make it risky to review agent output as if the prompt were a complete specification.

Make the reasoning auditable

For each generated change, record the requirement it addresses, the decision behind it, unresolved questions, and the test that demonstrates the intended behavior. A traceability matrix can help connect requirements to implementation and verification; use it as a working map, not as paperwork disconnected from the code (guide to a traceability matrix).

Ask the agent to state its assumptions and security consequences before reviewers accept the result. Then check whether those statements match the recorded decisions. If an assumption has no owner or an unresolved question affects behavior, pause the merge rather than letting the implementation resolve it without oversight.

Replay is useful when requirements or conversation context have changed. Rerun the task against the revised context and compare what the agent proposes, then verify that the tests still represent the current decision. This won't make outputs identical or prove correctness. It can reveal that an earlier implementation depended on stale assumptions, and it leaves a record of how the change relates to the conversation that produced it.

Designing a Human Review Workflow That Beats the Agent

Human review belongs at the end of a validation process, not in place of one. Reviewers should receive the diff alongside the prompt, relevant decisions, test results, and unresolved questions. Without that context, they can approve code that looks plausible while missing a requirement the agent never received or misunderstood.

Ask the author, or the agent before review, to answer concrete questions:

  • What requirement does this change satisfy, and what did you assume?
  • Which failure modes did you consider, and which remain untested?
  • What inputs or users can reach this code?
  • What security consequences follow from the design?
  • Which dependencies or permissions did you add, and why?
  • What test would fail if the implementation were wrong in the most likely way?

These prompts force attention onto intent and risk rather than line count. A small change to authorization can matter more than a large formatting diff, so scale review effort to the path the code touches.

Name reviewers for high-risk paths

Authentication, authorization, payments, cryptography, and sensitive data handling should require approval from a named expert. Don't leave that decision to whoever happens to be available or assume a successful scan clears the design. The reviewer needs to understand the system's trust boundaries and confirm that the implementation follows the team's intended controls.

For ordinary changes, review whether the tests express the requirement, whether the implementation introduces unnecessary complexity, and whether the agent's assumptions have been verified. The AI code review gap is a useful reminder to judge the workflow by what reviewers inspect, not by the presence of a review checkbox. A human remains accountable for acceptance; automation supplies evidence, not permission to skip judgment.

Wiring It All Into CI Without Slowing the Team Down

A useful CI pipeline separates fast feedback from deeper checks without allowing the fast path to become the only path. Run generated code in a constrained sandbox, then launch independent jobs for compilation, deterministic tests, property or fuzz checks, security scanning, and traceability validation. Fail quickly on clear blockers, but keep each job's result visible so teams know what passed and what didn't run.

A small team can assemble this with its existing CI runner, containers, a test framework, a static-analysis tool, a secret scanner, and dependency scanning. Store the prompt and decision record alongside build artifacts so reviewers can reproduce the generation context. Tools such as Stoa, from SpecStory, Inc., provide collaborative agents that write and run code in shared sandboxes, with outputs traceable to the conversation that produced them. Treat that capability as a way to preserve context, not as a substitute for your own acceptance gates.

Keep the pipeline's evidence separate

Track metrics independently. Compile-pass rate can show whether generated output meets basic build expectations; test-pass rate measures behavior against your suite. Mutation score indicates whether tests detect deliberately introduced faults, while property-violation rate records failures of stated invariants. Security findings per thousand lines can help teams compare scan results across changes, and reviewer-acceptance rate can show how often candidates survive human scrutiny.

None of these measures is a standalone quality score. A low number of security findings doesn't mean a feature behaves correctly, and a high reviewer-acceptance rate can reflect weak review rather than strong output. Compare trends within the same workflow and investigate unexpected changes instead of combining the metrics into a single badge.

CI metrics to track for AI-generated code

MetricWhat it measuresWhat it catches that others miss
Compile-pass rateWhether generated code type-checks and buildsBasic interface and compilation failures
Test-pass rateWhether candidates satisfy deterministic testsBehavior failures represented in the suite
Mutation scoreWhether tests detect deliberate code changesTests that pass without observing important behavior
Property-violation rateWhether generated inputs break stated invariantsEdge cases absent from hand-written examples
Security findings per thousand linesSecurity issues detected relative to code volumeRisk that functional metrics don't represent
Reviewer-acceptance rateWhether human reviewers accept the outputGaps that automated checks oracles don't capture

A team can start with this checklist:

  • Sandbox: Is execution isolated, constrained, and reproducible? Skip it, and a test run can expose shared systems or data.
  • Deterministic tests: Do compile, unit, integration, and contract checks express the intended behavior? Skip them, and the code may not meet the requirement.
  • Property and security gate: Are invariants, adversarial cases, static analysis, secrets, and dependencies checked? Skip them, and edge cases or vulnerabilities can survive a green suite.
  • Traceability: Can reviewers connect the change to a requirement, decision, and test oracle? Skip it, and stale context can pass as an approved design.
  • Human review: Has the right person examined assumptions and high-risk code paths? Skip it, and the team has no accountable acceptance decision.

Put the prompt, test results, security findings, and unresolved questions in the pull request. Reviewers can then ask what the agent assumed, which failure modes remain, and what evidence supports the change, instead of trying to reconstruct intent from the diff alone.


SpecStory, Inc. builds Stoa, a multiplayer AI workspace that captures product conversations, decisions, and open questions as executable context, with collaborative agents that write and run code in shared sandboxes. If your team wants generated changes to remain traceable to the discussions that shaped them, visit SpecStory, Inc. to explore the workflow.

Newsletter

Get new posts in your inbox

Bring your team together to build better products. Fresh takes on remote collaboration and AI-driven development.