responses mocks HTTP requests made with the requests library. pip install responses. import responses, import requests. Activate: @responses.activate def test_fn(): responses.add(responses.GET, "https://api.example.com/users", json=[...], status=200). resp = requests.get("https://api.example.com/users"). Add methods: responses.add(responses.POST, url, json={...}, status=201). Body: responses.add(..., body="raw text"). Headers: responses.add(..., headers={"X-Rate-Limit": "100"}). Match URL: exact by default, match_querystring=True to include query params. Callback: def req_callback(request): return (200, {}, json.dumps({"dynamic": True})), responses.add_callback(responses.GET, url, callback=req_callback). Passthrough: responses.add_passthrough("https://real.api.com"). Calls: responses.calls[0].request.body. len(responses.calls). Context: with responses.RequestsMock() as rsps: rsps.add(...). Assert called: assert responses.assert_call_count(url, 1). Multiple: add same URL multiple times — responses consumed in order. Error: responses.add(responses.GET, url, body=ConnectionError("timeout")). Match body: from responses import matchers, responses.add(..., match=[matchers.json_params_matcher({"key":"val"})]). Query match: match=[matchers.query_param_matcher({"page":"1"})]. URL re: responses.add(responses.GET, re.compile(r"https://api\.example\.com/users/\d+"), ...). Reset: responses.reset(). Claude Code generates responses mock layers for API clients, authentication flows, and paginated endpoint tests.
CLAUDE.md for responses
## responses Stack
- Version: responses >= 0.25
- Activate: @responses.activate decorator or responses.RequestsMock() context
- Add: responses.add(responses.GET/POST/PUT/..., url, json=, status=)
- Body: json= for dict (auto Content-Type) | body= for raw string/bytes
- Callback: responses.add_callback(method, url, callback=fn)
- Match: matchers.json_params_matcher | matchers.query_param_matcher
- Inspect: responses.calls[i].request | responses.assert_call_count(url, n)
- Passthrough: responses.add_passthrough(url) for real requests
responses HTTP Mocking Pipeline
# tests/responses_pipeline.py — HTTP mocking with the responses library
from __future__ import annotations
import json
import re
from typing import Any
import pytest
import requests
import responses
from responses import matchers
# ── 0. Code under test ────────────────────────────────────────────────────────
# These are sample API client functions we want to test with mocked HTTP.
BASE_URL = "https://api.example.com/v1"
def get_user(user_id: int) -> dict:
"""Fetch a user by ID from the API."""
resp = requests.get(f"{BASE_URL}/users/{user_id}")
resp.raise_for_status()
return resp.json()
def create_user(name: str, email: str) -> dict:
"""Create a new user via POST."""
resp = requests.post(f"{BASE_URL}/users", json={"name": name, "email": email})
resp.raise_for_status()
return resp.json()
def list_users(page: int = 1, limit: int = 20) -> dict:
"""Fetch paginated user list."""
resp = requests.get(f"{BASE_URL}/users", params={"page": page, "limit": limit})
resp.raise_for_status()
return resp.json()
def update_user(user_id: int, updates: dict) -> dict:
"""Update user fields."""
resp = requests.patch(f"{BASE_URL}/users/{user_id}", json=updates)
resp.raise_for_status()
return resp.json()
def delete_user(user_id: int) -> bool:
"""Delete a user. Returns True on success."""
resp = requests.delete(f"{BASE_URL}/users/{user_id}")
return resp.status_code == 204
def get_with_retry(url: str, max_retries: int = 3) -> dict:
"""Fetch with simple retry on 5xx errors."""
for attempt in range(max_retries):
resp = requests.get(url)
if resp.status_code < 500:
resp.raise_for_status()
return resp.json()
resp.raise_for_status()
return {}
class PaginatedFetcher:
"""Fetches all pages of a paginated API."""
def __init__(self, base_url: str):
self.base_url = base_url
def fetch_all(self, endpoint: str) -> list[dict]:
items = []
page = 1
while True:
resp = requests.get(f"{self.base_url}{endpoint}",
params={"page": page, "limit": 100})
resp.raise_for_status()
data = resp.json()
items.extend(data.get("items", []))
if data.get("next_page") is None:
break
page = data["next_page"]
return items
# ── 1. Basic GET/POST tests ───────────────────────────────────────────────────
class TestGetUser:
@responses.activate
def test_get_user_success(self):
responses.add(
responses.GET,
f"{BASE_URL}/users/42",
json={"id": 42, "name": "Alice", "email": "[email protected]"},
status=200,
)
user = get_user(42)
assert user["id"] == 42
assert user["name"] == "Alice"
assert user["email"] == "[email protected]"
assert len(responses.calls) == 1
@responses.activate
def test_get_user_not_found(self):
responses.add(
responses.GET,
f"{BASE_URL}/users/999",
json={"error": "User not found"},
status=404,
)
with pytest.raises(requests.HTTPError) as exc_info:
get_user(999)
assert exc_info.value.response.status_code == 404
@responses.activate
def test_get_user_server_error(self):
responses.add(
responses.GET,
f"{BASE_URL}/users/1",
body=ConnectionError("Connection refused"),
)
with pytest.raises(ConnectionError):
get_user(1)
@responses.activate
def test_create_user_validates_body(self):
"""Verify the client sends the correct JSON body."""
responses.add(
responses.POST,
f"{BASE_URL}/users",
json={"id": 1, "name": "Bob", "email": "[email protected]"},
status=201,
match=[matchers.json_params_matcher({"name": "Bob", "email": "[email protected]"})],
)
result = create_user("Bob", "[email protected]")
assert result["id"] == 1
@responses.activate
def test_create_user_wrong_body_raises(self):
"""responses raises ConnectionError if the body matcher fails."""
responses.add(
responses.POST,
f"{BASE_URL}/users",
json={"id": 1},
status=201,
match=[matchers.json_params_matcher({"name": "Carol"})],
)
# Sending wrong body should cause match failure
with pytest.raises(Exception):
create_user("NotCarol", "[email protected]")
# ── 2. Query parameter matching ───────────────────────────────────────────────
class TestListUsers:
@responses.activate
def test_list_users_first_page(self):
responses.add(
responses.GET,
f"{BASE_URL}/users",
json={"items": [{"id": 1}], "total": 50, "page": 1},
status=200,
match=[matchers.query_param_matcher({"page": "1", "limit": "20"})],
)
result = list_users(page=1, limit=20)
assert result["total"] == 50
@responses.activate
def test_list_users_custom_limit(self):
responses.add(
responses.GET,
f"{BASE_URL}/users",
json={"items": [], "total": 0, "page": 2},
match=[matchers.query_param_matcher({"page": "2", "limit": "5"})],
)
result = list_users(page=2, limit=5)
assert result["page"] == 2
# ── 3. Dynamic callbacks ──────────────────────────────────────────────────────
class TestCallbacks:
@responses.activate
def test_dynamic_response(self):
"""Callback receives the request and returns a dynamic response."""
def user_callback(request):
user_id = int(request.url.split("/")[-1])
body = json.dumps({"id": user_id, "name": f"User {user_id}"})
return (200, {"Content-Type": "application/json"}, body)
responses.add_callback(
responses.GET,
re.compile(r"https://api\.example\.com/v1/users/\d+"),
callback=user_callback,
)
user_10 = get_user(10)
user_20 = get_user(20)
assert user_10["name"] == "User 10"
assert user_20["name"] == "User 20"
assert len(responses.calls) == 2
@responses.activate
def test_rate_limit_callback(self):
"""Simulate 429 on first call, 200 on retry."""
call_count = [0]
def rate_limit_callback(request):
call_count[0] += 1
if call_count[0] == 1:
return (429, {"Retry-After": "1"}, '{"error": "rate limited"}')
return (200, {"Content-Type": "application/json"}, '{"ok": true}')
responses.add_callback(
responses.GET,
f"{BASE_URL}/users",
callback=rate_limit_callback,
)
result = get_with_retry(f"{BASE_URL}/users")
assert result == {"ok": True}
assert call_count[0] == 2
# ── 4. Pagination tests ───────────────────────────────────────────────────────
class TestPaginatedFetcher:
@responses.activate
def test_fetches_all_pages(self):
"""Mock three pages; assert all items are returned."""
responses.add(
responses.GET, f"{BASE_URL}/items",
json={"items": [{"id": 1}, {"id": 2}], "next_page": 2},
match=[matchers.query_param_matcher({"page": "1", "limit": "100"})],
)
responses.add(
responses.GET, f"{BASE_URL}/items",
json={"items": [{"id": 3}, {"id": 4}], "next_page": 3},
match=[matchers.query_param_matcher({"page": "2", "limit": "100"})],
)
responses.add(
responses.GET, f"{BASE_URL}/items",
json={"items": [{"id": 5}], "next_page": None},
match=[matchers.query_param_matcher({"page": "3", "limit": "100"})],
)
fetcher = PaginatedFetcher(BASE_URL)
items = fetcher.fetch_all("/items")
assert [i["id"] for i in items] == [1, 2, 3, 4, 5]
assert len(responses.calls) == 3
# ── 5. Context manager approach ───────────────────────────────────────────────
class TestRequestsMockContext:
def test_update_user(self):
"""Using RequestsMock context instead of decorator."""
with responses.RequestsMock() as rsps:
rsps.add(
responses.PATCH,
f"{BASE_URL}/users/7",
json={"id": 7, "name": "Updated"},
status=200,
match=[matchers.json_params_matcher({"name": "Updated"})],
)
result = update_user(7, {"name": "Updated"})
assert result["name"] == "Updated"
def test_delete_user(self):
with responses.RequestsMock() as rsps:
rsps.add(responses.DELETE, f"{BASE_URL}/users/5", status=204)
assert delete_user(5) is True
# ── 6. Asserting call counts ──────────────────────────────────────────────────
class TestCallInspection:
@responses.activate
def test_request_headers_sent(self):
"""Verify a specific header was included in the request."""
responses.add(responses.GET, f"{BASE_URL}/users/1", json={"id": 1})
session = requests.Session()
session.headers.update({"Authorization": "Bearer secret-token"})
session.get(f"{BASE_URL}/users/1")
sent_auth = responses.calls[0].request.headers.get("Authorization")
assert sent_auth == "Bearer secret-token"
@responses.activate
def test_exactly_one_call(self):
responses.add(responses.GET, f"{BASE_URL}/users/1", json={"id": 1})
get_user(1)
responses.assert_call_count(f"{BASE_URL}/users/1", 1)
# ── Demo ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("responses HTTP Mocking Demo")
print("=" * 50)
print("\nRun with pytest to execute all HTTP mock tests:")
print(" pytest tests/responses_pipeline.py -v")
print("\nKey patterns:")
print(" @responses.activate — intercept all requests in the test")
print(" responses.add(GET, url, json=) — register mock response")
print(" match=[json_params_matcher] — validate request body")
print(" match=[query_param_matcher] — validate query string")
print(" responses.add_callback — dynamic response function")
print(" responses.calls[0].request — inspect what was sent")
print(" RequestsMock() context — alternative to decorator")
For the unittest.mock.patch("requests.get") alternative — patching requests.get at the module level returns a Mock that must be configured with .return_value.json.return_value = {...} for every level of the call chain, and any code that calls requests.Session().get() escapes the patch scope, while @responses.activate intercepts all HTTP traffic at the transport layer regardless of how requests is called (Session, session.get, requests.request), and responses.calls[0].request.body lets you assert exactly what JSON the client sent. For the httpretty library alternative — httpretty patches the socket layer which can interfere with pytest networking fixtures and multiprocessing, while responses patches requests at the adapter level (no socket monkey-patching), matchers.json_params_matcher validates the request body without string parsing, and add_callback returns dynamic responses derived from the request URL or body, making stateful pagination tests (three pages with different next_page values) straightforward with sequential responses.add calls. The Claude Skills 360 bundle includes responses skill sets covering @responses.activate and RequestsMock context, responses.add with json/body/headers/status, dynamic callbacks with add_callback, URL regex matching, json_params_matcher and query_param_matcher, pagination sequential mocking, ConnectionError simulation, call inspection and assert_call_count, and passthrough for selective real requests. Start with the free tier to try HTTP mocking code generation.