Browse documentation
Testing agents that call unlob
Three response shapes are worth a fixture each, and none of them need a real key.
An agent that only ever gets tested against a clean 200 will not have handled partial: true, a 429 with no retry-after, or a 402 the first time it meets one in production.
All of them are cheap to fixture and none require a real API key, so there is no reason to
find out how your agent handles them for the first time against a live account.
Four fixtures worth keeping
A partial result. The response is a normal 200 with results populated and
"partial": true. The agent should surface this as an incomplete answer, not report the
short list as everything there is. This is the single most consequential fixture to have,
because a model handed a short list with no explanation will say “nothing found” — see
Reading a result for why the field exists.
A 429 without retry-after. The hard-capped key that is out of credits. The agent should stop
and say so, not retry. Pair it with a 429 that does carry retry-after, so a test can
assert the two are handled differently rather than both being treated as “try again.”
A 503 at capacity. The server was too busy to take the request and said so
immediately, with a retry-after. The agent should sleep that value and retry, and should
not confuse it with the other 503 — no shard could serve the request — which carries no
retry-after and does not clear on a schedule.
A 402. The account-suspended case. Distinct from both 429s, and the one case where
retrying is not merely wasteful but will never succeed until someone acts outside the
agent’s own loop — settling billing in the console.
// fixtures/unlob-partial.json
{ "total": 3, "results": [ /* … */ ], "partial": true }
// fixtures/unlob-429-credits.http
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 600
x-ratelimit-remaining: 0
monthly credits used up, and this key stops at its allowance rather than billing overage
// fixtures/unlob-429-ratelimit.http
HTTP/1.1 429 Too Many Requests
retry-after: 23
x-ratelimit-remaining: 0
rate limit exceeded
The retry-after on a rate-limit refusal is a real number of seconds — the time until your
minute turns over — so fixture it as something other than 1. A client that passes a test
against retry-after: 1 by sleeping a hardcoded second will retry into the same window in
production and be refused again.
// fixtures/unlob-503-capacity.http
HTTP/1.1 503 Service Unavailable
retry-after: 1
server is at capacity; retry shortly
Where each framework hooks in
The framework guides on this site each show a real client wired to a real base URL, which is exactly the seam to intercept in a test. A few concrete points:
- httpx-based clients (the Python, Pydantic AI,
LlamaIndex and CrewAI examples) mock
cleanly with
respx, matching on the route and returning one of the fixtures above instead of callinghttpx.AsyncClient.get. - fetch-based clients (TypeScript, Mastra,
Vercel AI SDK) intercept at
fetchitself —mswor a hand-rolled stub — since none of these guides wrapfetchin anything deeper that would need its own mock. - Dependency-injected clients, shown on the Pydantic AI page’s
Depspattern, need no monkeypatching at all: construct the agent with a mock client directly, which is the reason that pattern is worth the small extra indirection over a module-level client. - MCP-adapter tools (
langchain-mcp-adapters, Mastra’sMCPClient, the Agents SDK’sMCPServerStreamableHttp) are the one case worth testing against a real connection rather than a mock, at least once: the adapter’s job is translating the MCP frame into the framework’s own tool shape, and a fixture at the HTTP layer would skip past the part most likely to have a real bug. Point a test run at a real key with a tight budget instead, and save the fixtures above for the tool-execution logic layered on top.
A worked assertion
Using the respx-mocked Deps pattern from the Pydantic AI page as the concrete case:
import respx
import httpx
@respx.mock
async def test_agent_reports_incomplete_on_partial():
respx.get("https://api.unlob.com/search").mock(
return_value=httpx.Response(200, json={"total": 3, "results": [], "partial": True})
)
client = httpx.AsyncClient(base_url="https://api.unlob.com")
result = await agent.run("test query", deps=Deps(client=client))
assert "incomplete" in result.output.lower()
The assertion is on the agent’s own language, not on the mock — the fixture only proves the
tool received partial: true; the test’s actual job is confirming the agent’s system prompt
and tool schema together turn that into a caveat the user sees, rather than a confident
short answer.
What this does not cover
None of this replaces a smoke test against the real API before shipping. A mock proves your
agent handles the shapes correctly once it sees them; it does not prove your base URL,
header name or key format are actually correct — x-api-key versus
Authorization: Bearer is an easy typo that every fixture above will happily pass through
unnoticed, because the mock never checks the request at all unless you assert on it too.
Assert on the outgoing request’s headers at least once per client, not only on the response
handling.
FAQ
Do I need a real API key to run these tests? No. Every fixture above is a canned response; nothing here calls the real API, and a suite built entirely on them can run with no key configured at all.
Should I test against the real API in CI? A single smoke test with a low-cost call
(GET /account is free and unauthenticated failure modes aside, costs nothing) is worth
having, separate from the fixtured suite, specifically to catch a wrong header name or a
revoked key before it reaches production.
What about testing the MCP handshake itself? That is what Troubleshooting and the Inspector are for — a one-off manual check when wiring up a new client, not something to fixture into a CI suite per commit.
Bottom line
Four fixtures — a partial result, both shapes of 429, an at-capacity 503, and a 402 —
cover the failure modes that actually change what an agent should do, and every framework on this site mocks
them at the same seam its guide already shows: the HTTP client. Test the language your agent
produces from each fixture, not just that the fixture was received.
Next
- Rate limits and credits — the two 429s and 402, in full
- Pydantic AI — the
Depspattern used in the worked assertion above - Troubleshooting — for the MCP handshake itself