By Sagar Shankaran, Founder of CallSphere
Learn how to mock LLM API calls in your AI agent tests using FakeLLM objects, response fixtures, and assertion patterns for fast, deterministic, cost-free unit tests.
Key takeaways
AI agents depend on LLM calls that are non-deterministic, slow, and expensive. A single GPT-4 call takes 2-10 seconds and costs tokens — making it impractical to run hundreds of tests on every commit. Unit tests must be fast, free, and repeatable, which means you need a strategy for replacing real LLM calls with controlled substitutes.
The core challenge is that LLM outputs vary between calls even with temperature=0. Your tests need to verify your agent's logic — tool selection, state management, output parsing — without coupling to the exact wording an LLM produces.
Create a drop-in replacement for your LLM client that returns predetermined responses.
flowchart LR
PR(["PR opened"])
UNIT["Unit tests"]
EVAL["Eval harness<br/>PromptFoo or Braintrust"]
GOLD[("Golden set<br/>200 tagged cases")]
JUDGE["LLM as judge<br/>plus regex graders"]
SCORE["Aggregate score<br/>and per slice"]
GATE{"Score regress<br/>more than 2 percent?"}
BLOCK(["Block merge"])
MERGE(["Merge to main"])
PR --> UNIT --> EVAL --> GOLD --> JUDGE --> SCORE --> GATE
GATE -->|Yes| BLOCK
GATE -->|No| MERGE
style EVAL fill:#4f46e5,stroke:#4338ca,color:#fff
style GATE fill:#f59e0b,stroke:#d97706,color:#1f2937
style BLOCK fill:#dc2626,stroke:#b91c1c,color:#fff
style MERGE fill:#059669,stroke:#047857,color:#fff
from dataclasses import dataclass, field
from typing import Any
@dataclass
class FakeLLM:
"""A deterministic LLM replacement for unit tests."""
responses: list[str] = field(default_factory=list)
call_log: list[dict] = field(default_factory=list)
_call_index: int = 0
def chat(self, messages: list[dict], **kwargs) -> dict:
self.call_log.append({"messages": messages, **kwargs})
response = self.responses[self._call_index]
self._call_index += 1
return {"role": "assistant", "content": response}
This pattern lets you pre-load a sequence of responses and later inspect exactly what your agent sent to the LLM.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
Store realistic LLM responses as fixtures so multiple tests can share them.
import pytest
import json
from pathlib import Path
@pytest.fixture
def tool_call_response():
"""Fixture simulating an LLM response that invokes a tool."""
return {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "search_database",
"arguments": json.dumps({"query": "open tickets", "limit": 10}),
},
}
],
}
@pytest.fixture
def fixture_dir():
return Path(__file__).parent / "fixtures"
def load_fixture(fixture_dir: Path, name: str) -> dict:
return json.loads((fixture_dir / f"{name}.json").read_text())
Storing fixtures as JSON files in a tests/fixtures/ directory keeps tests clean and makes it easy to update expected responses when your prompts change.
Use unittest.mock.patch to intercept LLM calls at the boundary.
from unittest.mock import patch, MagicMock
from my_agent.core import Agent
def test_agent_extracts_entities():
fake_response = MagicMock()
fake_response.choices = [
MagicMock(message=MagicMock(
content='{"entities": ["Acme Corp", "Jane Doe"]}',
tool_calls=None,
))
]
with patch("my_agent.core.openai_client.chat.completions.create") as mock_create:
mock_create.return_value = fake_response
agent = Agent()
result = agent.extract_entities("Contact Jane Doe at Acme Corp")
assert result == ["Acme Corp", "Jane Doe"]
mock_create.assert_called_once()
call_args = mock_create.call_args
assert any("extract" in str(m) for m in call_args.kwargs["messages"])
Focus your assertions on what your code controls, not on LLM output text.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
def test_agent_selects_correct_tool(fake_llm):
"""Verify the agent passes the right tools to the LLM."""
fake_llm.responses = ['{"action": "search", "query": "test"}']
agent = Agent(llm=fake_llm)
agent.run("Find recent orders")
call = fake_llm.call_log[0]
tool_names = [t["function"]["name"] for t in call["tools"]]
assert "search_orders" in tool_names
assert "delete_account" not in tool_names # safety check
def test_agent_retries_on_parse_failure(fake_llm):
"""Verify retry logic when LLM returns malformed JSON."""
fake_llm.responses = ["not json", '{"action": "search"}']
agent = Agent(llm=fake_llm, max_retries=2)
result = agent.run("Find orders")
assert len(fake_llm.call_log) == 2 # retried once
assert result["action"] == "search"
Create an async generator fixture that yields predetermined chunks. Replace the streaming client method with this generator using patch. This lets you test your chunk-assembly logic without a real stream.
temperature=0 instead of mocking?Setting temperature=0 reduces variance but does not eliminate it — model updates can still change outputs. It also still costs tokens and takes seconds per call. Use temperature=0 for integration tests, but always mock for unit tests.
Keep a small, representative set: one normal response, one tool-call response, one refusal, one malformed response, and one empty response. Five to ten fixtures cover most agent logic paths without becoming a maintenance burden.
#UnitTesting #AIAgents #Mocking #Pytest #Python #Testing #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
BrowserStack offers 30,000+ real devices; Sauce Labs ships deep Appium automation. Here is how AI voice agent teams use both for WebRTC mobile QA in 2026.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
Meta is building Hatch, a consumer AI agent that operates DoorDash, Reddit, and other third-party apps — Meta's answer to OpenClaw and Google Remy.
OpenAI Frontier — the new enterprise platform announced this week for building, deploying, and managing AI agents that do real work.
Q1 2026 saw a record acquisition wave: Aircall bought Vogent (May), Meta acquired Manus and PlayAI, OpenAI closed six deals. The voice AI consolidation phase has begun.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco