Mock API Responses
Testing Code That Calls the Client
Lesson 181 tested the functions that build requests and read responses. Real application code also makes the call, and the call is the part that cannot be tested with ordinary data, because it reaches the network, costs money, and returns answers that vary. A mock solves this. A mock is a stand-in object that takes the place of the real client during a test. It records how it was called, and it returns whatever response the test specifies. The code under test runs unchanged, and the test controls both the input and the output of the call.
Mocks let a test answer two questions that matter most. Did the application send the right request? And did it handle the response it received correctly? Neither question needs the real API, and both can be answered in milliseconds, with no key and no bill.
The Code Under Test
The application function below summarizes a text by calling the client. It has the typical shape: it builds a request, makes the call, and reads the answer. Its only dependency is the client, which is passed in as a parameter, so a test can supply a different one:
"""summarizer.py: a function that calls the client, written so a test can supply the client."""
def summarize(client, text: str) -> str:
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=200,
messages=[{"role": "user", "content": f"Summarize: {text}"}],
)
return "".join(block.text for block in response.content if block.type == "text")
Passing the client in, rather than creating it inside the function, is the design decision that makes the function testable. A function that creates its own client reaches the network every time it runs, and it can be tested only by patching the module that creates the client. Both approaches work, but the parameter is simpler and more explicit.
Building a Mock
The standard library's unittest.mock module provides MagicMock, which accepts any attribute or method and records every call made to it. The test configures the return value of the method the code uses, and then inspects the recorded call. The fake response is a simple object with a content list, built with SimpleNamespace, which matches the attributes the code reads:
"""test_summarizer_demo.py: demonstrates a mock client without any network access."""
from types import SimpleNamespace
from unittest.mock import MagicMock
from summarizer import summarize
client = MagicMock()
client.messages.create.return_value = SimpleNamespace(
content=[SimpleNamespace(type="text", text="Short summary.")]
)
result = summarize(client, "a long article about loops")
print("result:", result)
client.messages.create.assert_called_once()
kwargs = client.messages.create.call_args.kwargs
print("model:", kwargs["model"], "max_tokens:", kwargs["max_tokens"])
print("prompt:", kwargs["messages"][0]["content"])
Output:
result: Short summary.
model: claude-sonnet-5-5 max_tokens: 200
prompt: Summarize: a long article about loops
How It Works
MagicMock()stands in for the client. Any call on it succeeds, so a test does not have to configure every attribute the code might touch.return_valuesets the response. Whenclient.messages.createis called, it returns the fake response, which the function reads exactly as it would read a real one.assert_called_once()checks the call count. A function that calls the API twice by mistake fails this check, which catches duplicated requests and their cost.call_args.kwargsexposes the arguments. The test reads back the model, the token limit, and the prompt, and checks them against what the application should send. This is the most valuable check: it confirms that the request is right before it is ever sent.- The response is checked against the function's output. The function returns the text it read from the response, so the output confirms that the reading logic works.
Asserting on Requests
Checking the request is often more useful than checking the answer, because the answer comes from the mock and proves only that the reading works. The request shows what the application actually asks the model to do. Tests can assert on any part of it: the model name, the limit, the presence of a system prompt, the number of messages, or the presence of a tool definition. A change that silently alters the prompt, such as dropping the instruction to summarize, fails a test immediately rather than surfacing as worse answers in production.
Writing the Test
The demonstration above is a script. As a test, the same logic becomes a function that pytest runs, with the assertions written as expectations:
from unittest.mock import MagicMock
from types import SimpleNamespace
from summarizer import summarize
def test_summarize_sends_expected_request():
client = MagicMock()
client.messages.create.return_value = SimpleNamespace(
content=[SimpleNamespace(type="text", text="Short summary.")]
)
assert summarize(client, "loops") == "Short summary."
kwargs = client.messages.create.call_args.kwargs
assert kwargs["model"] == "claude-sonnet-5-5"
assert kwargs["max_tokens"] == 200
assert kwargs["messages"][0]["content"] == "Summarize: loops"
The test checks both the request and the reading in one place, and it runs with python -m pytest in the same way as Lesson 181's tests.
Patching Instead of Passing
Sometimes a function creates its own client, and cannot be changed to accept one. In that case the test can replace the client where it is looked up, using unittest.mock.patch. The patch is a context manager, and it restores the original when the test ends:
from unittest.mock import MagicMock, patch
with patch("summarizer_module.client") as fake_client:
fake_client.messages.create.return_value = ...
result = summarize_module.run("loops")
Patching works, but it ties the test to where the name is looked up, so moving the code breaks the test. Passing the client as a parameter, as the example does, is the more robust design, and patching is the fallback for code that cannot be changed.
What a Mock Cannot Show
A mock returns exactly what it is told to return, so it cannot show whether the real service would have answered the same way. It cannot reveal that a parameter name is misspelled in a way the mock tolerates, unless the test asserts on the request, which is why asserting on the request is essential. It also cannot show whether a real response has the shape the mock assumes. The shape should be checked against real responses from time to time, using captured fixtures (Lesson 181), and the behavior against real calls with the evaluations in Lesson 110.
Summary
A mock stands in for the client during a test, records the calls made to it, and returns chosen responses. Passing the client as a parameter makes the function testable without patching. The most valuable assertions check the request, because a wrong request fails before any money is spent, and the response reading is checked against the mock's chosen output. Mocks cannot show how the real service behaves, and they must be complemented by captured real responses and by evaluation. The next lesson applies the same approach to the error paths.