Office Hours — How do you validate and test code generated by LLMs before deploying it to production?
A daily developer question about AI/LLMs, answered with a direct, opinionated take.
How do you validate and test code generated by LLMs before deploying it to production?
The honest answer: most teams don’t have a systematic approach yet, and the ones that do are building custom infrastructure because nothing off-the-shelf covers the full surface area. You’re not looking for a tool—you’re looking for a testing philosophy that accounts for the fact that LLM-generated code is fundamentally different from human-written code in failure modes.
The Real Problem with LLM Code
LLM-generated code looks correct. It’s syntactically valid, often well-structured, and occasionally even idiomatically sound. That’s precisely why it’s dangerous. Hallucinations don’t surface as syntax errors or type mismatches; they surface as logic that passes shallow tests but fails under real load or edge cases.
A Claude or GPT-5.6 Sol agent can confidently write a SQL query that looks perfect but uses the wrong join cardinality, or generate a regex that compiles but doesn’t match the intended pattern, or produce async code that deadlocks under specific concurrency patterns. The model is “eloquent, convincing, and confidently wrong”—and standard test coverage won’t catch it because the model’s wrong answer is internally coherent.
Mandatory Layers Before Production
Static analysis first. Before runtime testing, run your generated code through linters, type checkers, and security scanners. Use tools like pylint, mypy, semgrep, or language-specific equivalents to catch obvious category errors. If an LLM generates Python code that violates your project’s type hints, that’s a structural signal—not that the code is definitely wrong, but that it deviated from intent in a way worth checking manually.
Example workflow:
# Generated code lands in a quarantine directory
llm_generated_code/ → lint → type-check → security-scan → human review
# Treat LLM code like untrusted third-party contributions
pylint --disable=locally-disabled llm_generated_code/*.py
mypy --strict llm_generated_code/
semgrep --config=p/security-audit llm_generated_code/
This catches maybe 15–20% of real issues, but it’s free and it filters noise before human attention enters the loop.
Unit tests with adversarial intent. Don’t just test the happy path—test boundary conditions, null inputs, type mismatches, and resource exhaustion. LLMs often omit error handling or assume inputs are well-formed. Write tests that deliberately break assumptions:
# Generated function: calculate_monthly_fee(usage_gb: int) -> float
# Model probably assumed: usage_gb >= 0
def test_negative_usage():
assert calculate_monthly_fee(-100) == 0 # or raises, depending on contract
def test_zero():
assert calculate_monthly_fee(0) == 0
def test_massive_usage():
# Does it overflow? Does it clip? Does it explode?
result = calculate_monthly_fee(10**9)
assert 0 <= result <= 10**15 # sanity bound
def test_missing_tiers():
# If pricing has tiers, does the model handle all transitions?
assert calculate_monthly_fee(999) < calculate_monthly_fee(1000)
Generate these adversarial tests independently of the LLM-generated implementation. If the model wrote both the code and the tests, you’ve got a correlated failure mode.
Integration testing on real (or realistic) data. Unit tests pass. Integration breaks. Run the generated code against actual or representative production data in a staging environment. If it’s a query or database operation, check:
- Does it produce the right number of rows?
- Are joins producing duplicates or missing records?
- Do aggregations match a manual sanity check?
- Does performance degrade on larger datasets?
For APIs or external calls, use chaos engineering patterns: inject latencies, timeouts, malformed responses, and verify the generated code doesn’t crash or hang.
Differential testing against known-good baselines. If you’re replacing an existing function or rewriting legacy code, compare outputs between the old implementation and the LLM version on a large test set. Diffs reveal subtle logic divergences:
# Assuming old_calculate_fee and new_calculate_fee
test_cases = [
(0, None), # no data
(1, {}), # minimal input
(100, {"tiers": [...]}), # realistic
# ... 10k more cases from production logs
]
diffs = []
for inputs, context in test_cases:
old = old_calculate_fee(*inputs, context)
new = new_calculate_fee(*inputs, context)
if old != new:
diffs.append((inputs, old, new))
# Review diffs. Some are expected refactors. Some are bugs.
Handling Agent Code Autonomously
If an LLM agent is writing and committing code without human review on every step (e.g., Claude Code, Cursor Agent), you need guardrails that fire before merge:
- Pre-commit hooks that block commits if linting, type-checking, or security scans fail.
- CI gates that require test coverage and passing integration tests before merge to main.
- Code review automation that flags generated code for human review (not blocking, but flagging). Tools like Reviewpad or custom GitHub Actions can detect commits from agents and demand approval from a human before landing on protected branches.
- Rollback automation so if generated code deploys and fails fast enough, you can roll back without manual intervention.
One team I know routes all agent-generated PRs through a separate CI pipeline that runs extra strict tests and requires a human sign-off before merging to main. Agent code can merge to a staging branch freely, but production requires human approval.
The Cost-Benefit Trap
Here’s the catch: comprehensive testing of LLM-generated code is expensive. You’re running more tests, more carefully, on every artifact. Some teams rationalize this away by saying “the model is so good now it’s probably fine,” then hit a production incident when it isn’t.
The actual tradeoff is: you can deploy LLM code faster (agents don’t get tired), but you can’t deploy it less carefully. If anything, you should test it more rigorously because the failure modes are less familiar to your team. You don’t have intuition for what a Claude hallucination looks like at scale.
One concrete benchmark: Databricks benchmarked LLM-generated code on its million-line codebase and found that the GLM-5.2 model matched Claude Opus 4.8 in task completion rate (~68%) but required the same amount of human review and fixes. The speed win was real—agents handled grunt work—but the “trust but verify” overhead didn’t disappear.
What Actually Works in Practice
Teams with production LLM code workflows are doing:
- Separate test harnesses for generated vs. human code. Not because one is inherently better, but because failure modes differ. Generated code tends to fail on edge cases and concurrency; human code tends to fail on architectural decisions.
- Automated diff review that surfaces changes to critical paths (database mutations, API calls, auth logic) for human inspection, even if tests pass.
- Staged deployments where agent-generated code goes to 5% of traffic first, monitored for error rates, latency, and anomalies.
- Explicit approval gates for generated code touching security, payments, or data deletion—even in mature teams.
Bottom line:
Treat LLM-generated code as untrusted third-party contributions: static analysis first, adversarial unit tests second, integration testing on real data third, and human review on anything touching production systems or sensitive logic. Automation handles the bulk, but the last 5% of assurance requires human judgment.
Question via Hacker News