AI agents
Key points

Key points

  1. 1Record real tool responses once, then serve them from a local fake during tests to remove network latency and side effects.
  2. 2Match requests on method, URL and body so the fake knows which recorded reply to return.
  3. 3Inject the fake through your agent's tool interface rather than patching internals, so tests exercise the real orchestration code.
  4. 4Keep a small set of real-call contract tests for APIs that change often, and run mocks for everything else in the inner loop.

Why mock tool responses at all

An agent test fails in three different places: the model's reasoning, your orchestration code, and the external API. If all three run in the same test, a red build tells you almost nothing about which one broke. Mocking the tool boundary removes the third variable, and it also removes the two things that make agent tests unpleasant to run: waiting on the network and the risk of changing real data.

The classic integration-testing argument applies directly. When code under test depends on another system, you substitute a controlled stand-in so the test runs predictably without side effects as described in this discussion of external API tests. That conversation also flags the awkward case agent developers hit constantly: a POST endpoint with side effects and no sandbox. Mocking is often the only honest option.

There is a second, agent-specific reason. Tool design, not model capability, drives most agent failures, and vague schemas or weak error handling produce failures that are predictable rather than accidental per this analysis of agent tool design. Deterministic mocks let you assert on exactly what the agent sent: the tool name, the arguments, the argument types. That is how you catch a schema problem before a user does.

Mocks also let you test the unhappy paths that are hardest to trigger live. You cannot wait for a real rate limit to fire in CI, but you can serve a recorded 429 in milliseconds. Error responses need to tell the agent what to do next, because a bare "Bad Request" leaves a reasoning model with no way to self-correct argued here in detail. Mocks are how you prove your error payloads actually contain that guidance.

Step 1: Inventory the tools your agent can call

Before writing a single fake, list every external call the agent can make. Grep for your tool registration code and write the list down. For each tool, record five fields:

FieldWhy it matters for mocking
Tool nameThe key you will match on when the fake is called
Underlying transportWhether the real call is HTTP, a database query or a shell command
Method and URL patternPart of the request match key
Required and optional parametersDetermines how strict your body matching can be
Response shapes (success and error)The fixtures you need to record

This inventory pays for itself twice. Tool names are part of the model's decision signal, so a name that encodes both action and target (get_customer_by_email, not lookup) reduces wrong-tool selection according to these design patterns for tool use. Writing the list forces you to notice overlapping tools and ambiguous parameter names before they cause flaky tests.

Treat the inventory as a specification. A tool specification is the contract between the model and your code, covering the name, the description and the input schema as explained in this walkthrough of tool specifications. Your fake must honor the same contract, otherwise the test passes against a fiction.

Step 2: Record real responses once

Recording is a one-time cost per scenario. The cleanest approach is a thin proxy that sits between your tool implementation and the real API, writes each request and response pair to disk, and then gets switched off. This is the same idea as capturing production traffic through a sidecar or proxy and replaying it later, which is how teams validate services against realistic workloads without touching live systems described in this traffic replay pipeline guide.

Three rules make recordings useful:

  1. Sanitize before you commit. Strip auth headers, cookies and personal data. Recorded traffic is valuable precisely because it is realistic, which is also why it needs anonymizing before it lands in a repository per this discussion of replay without production risk.
  2. Record the ugly cases, not just the happy path. Capture at least one 4xx, one 5xx, one empty result and one rate-limit response per tool. A fake that only knows success will hide the bugs you most want to find.
  3. Store one file per scenario, named for the tool and case (search_contacts__empty_result.json). Flat names beat nested folders when you are debugging a failing test at speed.

Keep recordings small. Save the exact JSON body the API returned, plus the status code and the content type. Do not save timing data: latency is noise in a mock, and you want the fixture to be stable.

Step 3: Build a fake that matches on method, URL and body

A fake is a lookup table. Given an outgoing request, find the recorded response whose request matches. Match in layers, from cheapest to strictest:

import json
from pathlib import Path

FIXTURES = Path("tests/fixtures")

def load_fixtures():
    return [json.loads(p.read_text()) for p in FIXTURES.glob("*.json")]

def fake_call(method, url, body):
    candidates = [
        f for f in load_fixtures()
        if f["request"]["method"] == method and f["request"]["url"] == url
    ]
    for f in candidates:
        if json.loads(f["request"]["body"]) == body:
            return f["response"]["status"], f["response"]["body"]
    raise LookupError(f"No fixture for {method} {url} with body={body}")

Two decisions matter here. First, decide how strict body matching should be. Exact equality catches accidental extra fields, but it breaks on key ordering and on fields like timestamps. Normalize before comparing: sort keys, drop volatile fields, compare the rest. Second, fail loudly on a miss. LookupError is correct. A fake that silently returns an empty object trains your tests to ignore real divergence.

You can go further and use a real HTTP-level mock server, which is the approach WireMock recommends for agent development: mock APIs let you test edge cases, simulate timeouts and rate limits, iterate quickly, and control the environment for reproducible runs per this WireMock guide to testing agent API interactions. A local fake server is worth it when your tool code uses a real HTTP client you do not want to special-case; a plain function fake is simpler when you control the tool layer.

Step 4: Inject the fake through the tool interface

If your agent constructs its own HTTP client deep inside a tool, you cannot swap it cleanly. The fix is architectural: pass dependencies in. Mocking frameworks describe three levels of this. At the simplest, you mock the tool function itself to test tool selection and parameter passing. At the second, you mock the API or service the tool calls. At the third, you design for testability with dependency injection from the start, which is the most flexible option but requires planning as laid out in this guide to mocking external APIs in agent tests.

For fast agent tests, aim for a hybrid: the tool function is real (so you test your code), and its client dependency is injected (so you can swap in the fake). A sketch:

class SearchContactsTool:
    def __init__(self, client):
        self.client = client

    def __call__(self, query: str) -> dict:
        status, body = self.client.get("/contacts", {"q": query})
        if status >= 400:
            return {"error": "contact search failed", "hint": "retry with a full name"}
        return body

In tests, pass a client that calls fake_call. In production, pass the real one. The agent sees no difference, and your test now covers the tool's error handling without a network. This is also where you encode the agent-facing error contract: what failed, why, and how to fix it, in the shape recommended for agent consumers in this error-handling writeup.

What to assert, and when to use real calls instead

Fast tests are only useful if they fail for the right reason. Assert on three things per scenario: the call itself (which tool, which arguments, in which order), the rendered result (how your code turned the response into what the model sees), and the error path (serve a recorded 429 or 500 and assert the agent's next move is sensible rather than a blind retry). Validating the final answer alone misses agents that reach a correct result through an unsafe or wasteful path, which is why trajectory-aware evaluation is recommended over outcome-only checks per this guide to testing AI agents.

Be careful about asserting on the model's exact output text. That is inherently non-deterministic and makes tests flaky. Assert on structure and on tool calls; keep free-text assertions coarse. Also validate tool results in the runtime before acting on them: a search that returns an empty array should trigger a reformulated query, not a final "nothing found" answer, and that check belongs in code, not in the model's reasoning as argued in these tool use patterns.

Mocks are a complement to real calls, not a replacement. Use live calls when the thing you are testing is the contract itself.

SituationMockReal call
Inner-loop development, running hundreds of tests per minuteYesNo
Asserting tool selection, argument shape, error handlingYesNo
Verifying the API still accepts your request schemaNoYes, scheduled
Load, latency and rate-limit behaviorNoYes
API under active change, fields shifting weeklyNoYes, or re-record fixtures
Testing a POST with irreversible side effectsYesOnly against a sandbox

A useful split is a two-tier suite. Tier one is mocked, runs on every save, and must complete in seconds. Tier two is a small contract suite that hits the real API, runs on a schedule or before release, and exists to catch schema drift and deprecations. When tier two fails, you re-record the affected fixtures and tier one picks up the change.

There is a measured reason to invest in tool tests specifically. Automated tool testing found that many publicly available tools suffer from underspecification, and the testing method surfaced far more erroneous inputs than prompt-engineering approaches alone per the ToolFuzz paper. An automated pipeline that improved tool documentation raised task accuracy from 22.9% to 35.4% on GitHub tasks and from 29% to 39% on file management tasks according to the ToolFuzz record. Those numbers are about documentation, but the mechanism is the same one mocks enable: cheap, repeatable runs against a stable tool surface.

A worked example with numbers

Suppose your agent suite has 200 tests and each one triggers three tool calls. Against live APIs at 400 ms per call, tool time alone is 200 × 3 × 0.4 = 240 seconds, before any model latency. That is four minutes of pure waiting, and it fails whenever the API hiccups or a rate limit lands. Replace those 600 calls with local fixture lookups at roughly 1 ms each and tool time drops to about 0.6 seconds. The suite goes from "run it before lunch" to "run it on save."

The trade is that you have now frozen the API's behavior at recording time. If the real API renames a field next month, every mocked test still passes. That is exactly the failure mode the scheduled real-call tier exists to catch. Budget maybe 10 to 20 contract tests against 200 mocked ones: enough to detect drift, cheap enough to run nightly.

When you do need real calls across several providers while building the test harness, or when you want to swap model backends without rewriting client code, a single OpenAI-compatible endpoint for text, image and embedding models is the practical path rather than juggling keys. Quanta Inference offers exactly that on a prepaid, dollar-priced basis, which keeps the contract tier's cost predictable.

A handful of mistakes account for most flaky mock suites: patching a private method instead of injecting the client, matchers so loose that the body is ignored, recording only success, silent fallbacks that turn a broken fixture into a green build, and fixtures nobody ever revalidates against the real API. Each one is cheap to fix at design time and expensive to untangle later.

FAQ

FAQ

Do mocks make agent tests less realistic?

They make each test less end-to-end but more diagnostic. You trade realism for the ability to say precisely which layer broke, and you keep realism in the small contract tier that hits real APIs on a schedule. Teams that skip the contract tier are the ones surprised in production.

How strict should request matching be?

Match method and URL exactly, and normalize the body before comparing: sort keys, drop volatile fields like timestamps and request IDs, compare the rest. Exact byte-for-byte matching breaks too often to be worth the maintenance.

Should I mock the tool function or the HTTP client underneath it?

Mock the client underneath when you want to test your tool's parsing and error handling. Mock the tool function when you only want to test the agent's selection and argument generation. Dependency injection lets you choose per test.

What about tools that write data?

Mock them in the inner loop and point them at a sandbox for the rare real-call test. The classic problem of testing a POST with irreversible side effects is precisely the case where a recorded fake earns its keep.

How often should I re-record fixtures?

When the real-call contract suite fails, when an API announces a schema change, or on a fixed cadence such as monthly. Treat fixtures like any other test data: reviewed, sanitized and versioned alongside the code.

Sources

Sources

  1. How are integration tests written for interacting with ...
  2. AI Agent Tool Design: What Works and What Doesn't
  3. Error Handling for AI Agents: A Different Standard Than Classic Apps
  4. Tool Use in LLMs: Design Patterns for Reliable Agent Actions - Pristren
  5. P.E: 4.2, Why tool specifications matter in LLM APIs | by John Chiwai
  6. How to Build Production Traffic Replay Testing Pipelines ...
  7. Traffic Replay: Production Without Production Risk
  8. Test AI agents' API interactions using WireMock Cloud and ...
  9. Mocking External APIs in Agent Tests, Scenario
  10. How To Test AI Agents Effectively - Splunk
  11. ToolFuzz - Automated Agent Tool Testing
  12. ToolFuzz: Automated Agent Tool Testing
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles