Mock API Responses

Su Suriya Ravichandran Updated 10 Nov 2026
5 min read ·Lesson 182 of 200

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

  1. 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.
  2. return_value sets the response. When client.messages.create is called, it returns the fake response, which the function reads exactly as it would read a real one.
  3. 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.
  4. call_args.kwargs exposes 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.
  5. 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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Mock API Responses and get answers drawn from it.

Signed-in readers only.