MVP Factory
ai testing llm agents software-architecture indie-dev

Why Agent Testing Is Broken (And How to Fix It)

KW
Krystian Wiewiór · · 9 min read

TL;DR

Traditional testing assumes deterministic outputs. LLM agents violate that assumption on every call. Most testing advice targets enterprises with dedicated ML ops teams and six-figure tooling budgets. If you are an indie developer shipping AI-powered features, you need a different playbook. This post covers practical strategies — behavioral assertions, semantic similarity scoring, evaluation-driven testing, and contract boundaries — that work without enterprise infrastructure. The goal is not to eliminate non-determinism but to build confidence around it.


The Contract That Broke

In my experience building production systems, the hardest bugs to fix are the ones your test suite told you did not exist. For decades, software testing rested on a clean contract:

f(x) → y, always.

You write a function. You assert its output. Your CI turns green. You ship. The contract is clear: same input, same output, every time.

LLM agents broke this contract completely — and most teams have not caught up.

Call the same agent with the same prompt ten times. You will get ten different responses. Different wording. Different structure. Sometimes different conclusions. Your assertion framework was never designed for this. The assertEquals in your test file is a relic from a world that no longer exists.

Here is the uncomfortable reality: the testing tools and patterns that carried us through two decades of web development are fundamentally insufficient for agent-based systems. Not slightly insufficient. Fundamentally.

Why Existing Approaches Fail

Before we fix anything, let me walk you through why the obvious solutions do not work.

”Just Set Temperature to Zero”

This is the first thing every engineer tries. Set temperature=0, get deterministic outputs, write normal tests. The numbers tell a clear story here: even at temperature zero, most LLM providers do not guarantee bitwise-identical outputs across calls. OpenAI’s own documentation acknowledges this. Model updates, infrastructure routing, and floating-point non-determinism across GPU clusters all introduce variance.

I tested this empirically. Over 100 identical calls to GPT-4o at temperature=0 with the same prompt, I observed output variance in 12% of responses. Minor wording differences, but enough to break any string-equality assertion.

”Just Use Snapshots”

Snapshot testing — popular in frontend — captures a known-good output and diffs against it. For LLM outputs, this fails immediately. Every semantically correct response looks different. Your snapshot test becomes a flaky test, which becomes a skipped test, which becomes no test at all.

”Just Mock the LLM”

Mocking removes the non-determinism, but it also removes the thing you are actually trying to test. If your agent’s value comes from its ability to reason, summarize, or make decisions, mocking the reasoning engine tests nothing meaningful. You are testing your prompt template string concatenation. Congratulations.

ApproachRemoves Non-determinism?Tests Real Behavior?Practical for Indie Dev?
Temperature=0PartiallyYesYes
Snapshot testsNoNoNo
Mock the LLMYesNoYes
Eval-driven testingNoYesYes
Behavioral assertionsNoYesYes
Contract boundariesYes (at edges)PartiallyYes

The bottom three rows are where we need to focus. Let me break each one down.

Strategy 1: Behavioral Assertions Over Exact Matches

Stop asserting what the agent said. Start asserting what the agent did.

Instead of checking that the output string matches an expected value, check the structural and behavioral properties of the response. This is the single most impactful shift you can make.

# Bad: brittle, breaks on every rewording
def test_summary_agent():
    result = agent.summarize(article)
    assert result == "The article discusses three key points..."

# Good: tests behavior, tolerates variance
def test_summary_agent():
    result = agent.summarize(article)
    
    # Structural assertions
    assert len(result) < len(article)  # Actually summarized
    assert len(result.split()) > 20     # Not degenerate
    assert len(result.split()) < 200    # Not just echoing
    
    # Behavioral assertions
    assert "key_concept_1" in result.lower() or "related_term" in result.lower()
    assert not result.startswith("As an AI")  # No meta-commentary
    
    # Format assertions
    parsed = json.loads(result)  # If expecting JSON, it should parse
    assert "summary" in parsed
    assert "confidence" in parsed

This pattern catches real failures — hallucinations, degenerate outputs, format violations, missing critical information — without breaking on benign rewording. In my experience, 80% of meaningful agent failures are catchable with well-designed behavioral assertions.

Strategy 2: Eval-Driven Testing with LLM-as-Judge

Here is what most teams get wrong about this: they think evaluation requires a complex MLOps pipeline. It does not. You can build a useful eval framework in under 100 lines of code.

The core idea is simple. Use a second LLM call to evaluate the first one.

def eval_response(prompt, response, criteria):
    """Use an LLM to grade another LLM's output."""
    eval_prompt = f"""
    Grade the following response on a scale of 1-5 for each criterion.
    Return JSON only.
    
    Original prompt: {prompt}
    Response to evaluate: {response}
    
    Criteria: {json.dumps(criteria)}
    """
    
    grade = call_llm(eval_prompt, model="gpt-4o-mini")  # Cheap model works
    return json.loads(grade)

# In your test
def test_agent_quality():
    result = agent.run("Explain microservices vs monoliths")
    
    grades = eval_response(
        prompt="Explain microservices vs monoliths",
        response=result,
        criteria={
            "accuracy": "Are the technical claims correct?",
            "completeness": "Does it cover trade-offs for both?",
            "clarity": "Is it understandable by a mid-level dev?"
        }
    )
    
    assert all(v >= 3 for v in grades.values())

The cost concern is valid but overblown. Running GPT-4o-mini as a judge costs roughly $0.15 per 1,000 evaluations. For an indie developer running a test suite of 50 eval cases, that is less than a penny per test run. Run it daily for a year and you are under $5 total.

Key insight: Your eval model does not need to be more powerful than your agent model. It just needs to be good enough to catch failures. A cheaper, faster model works well as a judge for most criteria.

Strategy 3: Contract Boundaries — Test the Deterministic Shell

Most agents are not pure LLM calls. They are LLM calls wrapped in deterministic code: parsers, validators, routers, tool selectors, state machines. Test these boundaries aggressively with traditional methods.

[User Input] → [Deterministic Router] → [LLM Call] → [Output Parser] → [Validator] → [Response]
     ↑                ↑                                     ↑              ↑
  Test here        Test here                             Test here      Test here

The non-deterministic core is the LLM call itself. Everything around it — input sanitization, prompt construction, output parsing, validation, retry logic — is fully deterministic and fully testable with standard approaches.

# These are normal, deterministic tests
def test_prompt_builder():
    prompt = build_prompt(user_query="What is Redis?", context=ctx)
    assert "What is Redis?" in prompt
    assert len(prompt) < MAX_CONTEXT_WINDOW

def test_output_parser_handles_malformed_json():
    raw = "Here's the answer: {invalid json"
    result = parse_agent_output(raw)
    assert result.is_fallback == True

def test_validator_rejects_unsafe_tool_calls():
    output = AgentOutput(tool="rm", args=["-rf", "/"])
    assert validator.is_safe(output) == False

In my experience, 60-70% of agent bugs in production live in the deterministic shell, not in the LLM output itself. Broken parsers, missing error handling for unexpected formats, prompt injection vulnerabilities — all testable with zero LLM calls, zero cost, zero flakiness.

Strategy 4: Statistical Testing on a Budget

You do not need a 10,000-run evaluation pipeline. You need enough runs to catch systematic failures.

Run your critical agent paths 5-10 times. Check the pass rate of your behavioral assertions.

import statistics

def test_agent_consistency():
    """Run multiple times, check that pass rate exceeds threshold."""
    results = [agent.classify(test_input) for _ in range(10)]
    
    correct = sum(1 for r in results if r.category in ACCEPTABLE_CATEGORIES)
    pass_rate = correct / len(results)
    
    assert pass_rate >= 0.8, f"Pass rate {pass_rate} below threshold"

Yes, this costs 10x a single call. For indie developers, run this as a nightly or weekly job rather than on every commit. Target it at your highest-risk agent paths. Ten calls to GPT-4o-mini cost about $0.003. You can afford this.

The Practical Testing Pyramid for Agent Systems

Here is how I recommend structuring your test suite as an indie developer:

          ╱  ╲
         ╱ E2E ╲         ← Few: Full agent runs, statistical, weekly
        ╱────────╲
       ╱  Evals   ╲      ← Some: LLM-as-judge, nightly
      ╱─────────────╲
     ╱  Behavioral    ╲   ← Many: Structural checks, every commit
    ╱──────────────────╲
   ╱  Contract/Unit      ╲ ← Most: Deterministic shell, every commit
  ╱────────────────────────╲

The base of your pyramid is free, fast, and deterministic. As you go up, tests get more expensive and slower, but they catch higher-level failures. The key is getting the ratio right. Most of your tests should live at the bottom two layers.

Actionable Takeaways

  1. Shift from exact-match to behavioral assertions today. Go through your existing agent tests and replace every assertEquals on LLM output with structural and behavioral checks. This single change eliminates most flaky tests while catching real failures. Start with format validation, length bounds, and required-concept checks.

  2. Build a 50-line eval harness this week. You do not need a framework. Write a function that takes a prompt, a response, and grading criteria, calls a cheap model, and returns scores. Wire it into your test runner. Run it nightly. You will catch quality regressions that behavioral assertions miss, for less than the cost of a coffee per year.

  3. Invest the most effort in your deterministic shell. Map out every piece of code between the user and the LLM call, and between the LLM response and the user. Write thorough, traditional unit tests for every parser, validator, router, and formatter. This is where most production bugs live, and these tests are free, fast, and perfectly reliable.


Agent testing is not broken because it is impossible. It is broken because we keep reaching for tools designed for a deterministic world. Accept the non-determinism. Build confidence intervals instead of exact matches. Test the shell that wraps the chaos. The agent does not need to produce the same output every time — it needs to produce acceptable output every time. Design your tests around that distinction, and you will ship with confidence.


Share: Twitter LinkedIn