Code that calls Claude has two kinds of behavior worth testing, and only one of them needs a model. Whether the model gives good answers is an evaluation problem — run it deliberately, with real calls and human or automated grading. Whether your plumbing is correct — the loop terminates, tool results are injected in the right shape, errors are handled, the right things are logged — is an ordinary unit-testing problem, and real API calls make it slow, flaky, and expensive. Unit tests should mock; this article covers the three standard levels at which to do it.
Level 1: monkey-patching the SDK call
The simplest approach replaces the SDK method your code calls with a stub returning a canned response object. In Python with pytest:
def test_loop_stops_without_tool_calls(monkeypatch, client):
def fake_create(**kwargs):
return make_fake_message( # your test factory
content=[make_text_block("All done.")])
monkeypatch.setattr(client.messages, "create", fake_create)
result = run_agent(client, "do the thing")
assert result.final_text == "All done."
Build one small factory for fake messages (content blocks, stop reason, usage) and reuse it everywhere. For tool-loop tests, have the stub return a tool_use-bearing message on the first call and a plain answer on the second — that single fixture pattern exercises dispatch, result injection, and termination (see the tool loop). The same idea works in every SDK language; TypeScript teams typically use their test framework's module mocking, and the TypeScript SDK explicitly supports Jest 28+ in the node environment.
The weakness: patching create bypasses the SDK's own serialization, so a malformed request body would sail through. That is what level 2 fixes.
Level 2: intercepting HTTP
HTTP-intercept libraries (respx or responses in Python, nock in Node) stub the network layer instead of the method, so the real SDK code runs — building the request, setting headers, parsing your fake JSON response into real SDK types. Your test asserts on the actual outgoing request: correct endpoint, correct model ID, tool definitions serialized as intended. This catches a class of bug that method-level mocks cannot, at the cost of writing response JSON in the API's wire shape. Record it once from a real call (see VCR cassettes) rather than hand-crafting it from memory.
Level 3: a fake server via base-URL override
For integration-style tests — or polyglot systems where several services call Claude — run a local fake server and point the client at it. The Anthropic tooling supports overriding the API host (the ant CLI, for example, honors an ANTHROPIC_BASE_URL environment variable or --base-url flag; check your SDK's README for its equivalent option). A ten-line fake server that replays fixture responses gives you language-agnostic, process-level isolation.
Credential hygiene in the test environment
Two documented behaviors bite test suites regularly:
The SDK auto-reads ANTHROPIC_API_KEY. Client classes pick the key up from the environment automatically. That is convenient in production and dangerous in CI: a leaked real key in the runner environment means an "accidentally unmocked" test quietly makes real, billed calls. Set a dummy value in test config, and make an unexpected outbound HTTP call a test failure (most intercept libraries can enforce this).
Empty is not unset. In the documented credential-resolution order, an environment variable set to an empty string still occupies its precedence slot — ANTHROPIC_API_KEY="" selects the API-key path with an empty key rather than falling through to profiles or federation. When tests manipulate credentials, unset variables; don't blank them.
AnthropicBedrockMantle or AnthropicVertex.What not to mock
Keep a small, separately triggered suite of live smoke tests — real calls with a scoped key against the cheapest suitable model — to catch what mocks never will: authentication changes, serialization drift, deprecations. Run it on a schedule or before releases, not on every commit.
Where to go next
Recording and replaying API responses automates fixture creation for level 2 and 3. For the code under test, start with the Python tool loop.