respx mocks httpx HTTP calls in tests. pip install respx. Context manager: import respx; import httpx. with respx.mock: resp = httpx.get("https://api.example.com/users"); assert resp.status_code == 200. Route: respx.get("https://api.example.com/users").mock(return_value=httpx.Response(200, json=[{"id":1}])). Method shorthands: respx.get(url), respx.post(url), respx.put(url), respx.patch(url), respx.delete(url). Pattern match: respx.get(url__regex=r"/users/\d+"). respx.route(method="GET", url__startswith="https://api"). Headers match: respx.get(url, headers={"X-Token":"abc"}). Content match: respx.post(url, content=b"hello"). JSON match: respx.post(url, json={"email":"[email protected]"}). Response: httpx.Response(200, json={...}). httpx.Response(201, headers={"Location":"/items/1"}). httpx.Response(422, json={"detail":"validation error"}). side_effect: route.mock(side_effect=httpx.TimeoutException) — simulate timeout. side_effect=lambda req: httpx.Response(200, json={"path":str(req.url)}) — dynamic. Sequential: route.mock(side_effect=[resp1, resp2, httpx.ConnectError]) — cycle through responses. Async: async with respx.mock: resp = await async_client.get(url). @respx.mock async def test_async(...). Router: router = respx.Router(). router.get(url).mock(...). with router: .... pass_through: respx.mock(assert_all_called=False). respx.route(url__startswith="https://real").pass_through(). Decorator: @respx.mock def test_fn(respx_mock): respx_mock.get(url).mock(...). pytest: @pytest.fixture def mock_api(): with respx.mock: yield respx. Reset: respx.calls.reset(). assert: route.called → bool. route.call_count. route.calls.last.request.content. respx.calls — all calls. Claude Code generates respx mock routes, async test fixtures, and call assertion patterns.
CLAUDE.md for respx
## respx Stack
- Version: respx >= 0.21 | pip install respx
- Mock: with respx.mock: ... | @respx.mock decorator | respx.mock(assert_all_called=True)
- Route: respx.get(url).mock(return_value=httpx.Response(200, json={...}))
- Match: url__regex | url__startswith | headers | json for flexible matching
- Dynamic: route.mock(side_effect=lambda req: httpx.Response(...)) — inspect request
- Async: async with respx.mock — works with httpx.AsyncClient
- Assert: route.called | route.call_count | route.calls.last.request
respx HTTP Mocking Pipeline
# tests/test_api_client.py — respx mock usage
from __future__ import annotations
import json
from typing import Any
import httpx
import pytest
import respx
# ─────────────────────────────────────────────────────────────────────────────
# API client under test
# ─────────────────────────────────────────────────────────────────────────────
BASE_URL = "https://api.example.com"
class UserApiClient:
"""Synchronous httpx client wrapping a fictional User API."""
def __init__(self, base_url: str = BASE_URL, token: str = "token-xyz") -> None:
self._base_url = base_url.rstrip("/")
self._headers = {"Authorization": f"Bearer {token}"}
def list_users(self, page: int = 1, page_size: int = 20) -> list[dict]:
with httpx.Client() as client:
r = client.get(
f"{self._base_url}/users",
params={"page": page, "page_size": page_size},
headers=self._headers,
)
r.raise_for_status()
return r.json()
def get_user(self, user_id: int) -> dict:
with httpx.Client() as client:
r = client.get(
f"{self._base_url}/users/{user_id}",
headers=self._headers,
)
r.raise_for_status()
return r.json()
def create_user(self, payload: dict) -> dict:
with httpx.Client() as client:
r = client.post(
f"{self._base_url}/users",
json=payload,
headers=self._headers,
)
r.raise_for_status()
return r.json()
def update_user(self, user_id: int, payload: dict) -> dict:
with httpx.Client() as client:
r = client.patch(
f"{self._base_url}/users/{user_id}",
json=payload,
headers=self._headers,
)
r.raise_for_status()
return r.json()
def delete_user(self, user_id: int) -> None:
with httpx.Client() as client:
r = client.delete(
f"{self._base_url}/users/{user_id}",
headers=self._headers,
)
r.raise_for_status()
class AsyncUserApiClient:
"""Async httpx client for the same API."""
def __init__(self, base_url: str = BASE_URL, token: str = "token-xyz") -> None:
self._base_url = base_url.rstrip("/")
self._headers = {"Authorization": f"Bearer {token}"}
async def list_users(self, page: int = 1) -> list[dict]:
async with httpx.AsyncClient() as client:
r = await client.get(
f"{self._base_url}/users",
params={"page": page},
headers=self._headers,
)
r.raise_for_status()
return r.json()
async def create_user(self, payload: dict) -> dict:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self._base_url}/users",
json=payload,
headers=self._headers,
)
r.raise_for_status()
return r.json()
# ─────────────────────────────────────────────────────────────────────────────
# pytest fixtures
# ─────────────────────────────────────────────────────────────────────────────
@pytest.fixture
def client() -> UserApiClient:
return UserApiClient(token="test-token")
@pytest.fixture
def async_client() -> AsyncUserApiClient:
return AsyncUserApiClient(token="test-token")
@pytest.fixture
def mock_users() -> list[dict]:
return [
{"id": 1, "email": "[email protected]", "first_name": "Alice", "role": "admin"},
{"id": 2, "email": "[email protected]", "first_name": "Bob", "role": "user"},
]
# ─────────────────────────────────────────────────────────────────────────────
# 1. Basic synchronous mocking — context manager
# ─────────────────────────────────────────────────────────────────────────────
class TestListUsers:
def test_returns_user_list(self, client: UserApiClient, mock_users: list[dict]) -> None:
with respx.mock:
respx.get(f"{BASE_URL}/users").mock(
return_value=httpx.Response(200, json=mock_users)
)
result = client.list_users()
assert len(result) == 2
assert result[0]["email"] == "[email protected]"
def test_empty_list(self, client: UserApiClient) -> None:
with respx.mock:
respx.get(f"{BASE_URL}/users").mock(
return_value=httpx.Response(200, json=[])
)
result = client.list_users()
assert result == []
def test_server_error_raises(self, client: UserApiClient) -> None:
with respx.mock:
respx.get(f"{BASE_URL}/users").mock(
return_value=httpx.Response(500, json={"detail": "Internal error"})
)
with pytest.raises(httpx.HTTPStatusError):
client.list_users()
class TestGetUser:
def test_returns_user(self, client: UserApiClient) -> None:
user = {"id": 1, "email": "[email protected]", "role": "admin"}
with respx.mock:
respx.get(f"{BASE_URL}/users/1").mock(
return_value=httpx.Response(200, json=user)
)
result = client.get_user(1)
assert result["email"] == "[email protected]"
def test_not_found_raises(self, client: UserApiClient) -> None:
with respx.mock:
respx.get(f"{BASE_URL}/users/999").mock(
return_value=httpx.Response(404, json={"detail": "User not found"})
)
with pytest.raises(httpx.HTTPStatusError) as exc_info:
client.get_user(999)
assert exc_info.value.response.status_code == 404
# ─────────────────────────────────────────────────────────────────────────────
# 2. POST with request body inspection
# ─────────────────────────────────────────────────────────────────────────────
class TestCreateUser:
def test_creates_and_returns_user(self, client: UserApiClient) -> None:
created = {"id": 3, "email": "[email protected]", "role": "user"}
with respx.mock:
route = respx.post(f"{BASE_URL}/users").mock(
return_value=httpx.Response(201, json=created)
)
result = client.create_user({"email": "[email protected]", "role": "user"})
assert result["id"] == 3
# Verify request was made
assert route.called
assert route.call_count == 1
# Inspect the request body
sent = json.loads(route.calls.last.request.content)
assert sent["email"] == "[email protected]"
def test_validation_error_raises(self, client: UserApiClient) -> None:
with respx.mock:
respx.post(f"{BASE_URL}/users").mock(
return_value=httpx.Response(422, json={"detail": [{"msg": "field required"}]})
)
with pytest.raises(httpx.HTTPStatusError):
client.create_user({})
# ─────────────────────────────────────────────────────────────────────────────
# 3. side_effect — dynamic responses and error simulation
# ─────────────────────────────────────────────────────────────────────────────
class TestSideEffects:
def test_network_timeout(self, client: UserApiClient) -> None:
"""Simulate a timeout exception."""
with respx.mock:
respx.get(f"{BASE_URL}/users").mock(
side_effect=httpx.TimeoutException("Connection timed out")
)
with pytest.raises(httpx.TimeoutException):
client.list_users()
def test_connect_error(self, client: UserApiClient) -> None:
with respx.mock:
respx.get(f"{BASE_URL}/users").mock(
side_effect=httpx.ConnectError("Connection refused")
)
with pytest.raises(httpx.ConnectError):
client.list_users()
def test_dynamic_response_from_request(self, client: UserApiClient) -> None:
"""side_effect callable receives the request — use it to echo request data."""
def echo_params(request: httpx.Request) -> httpx.Response:
page = request.url.params.get("page", "1")
return httpx.Response(200, json={"page": int(page), "items": []})
with respx.mock:
respx.get(f"{BASE_URL}/users").mock(side_effect=echo_params)
result = client.list_users(page=3)
assert result["page"] == 3
def test_sequential_responses(self, client: UserApiClient) -> None:
"""Return different responses on each call."""
responses = [
httpx.Response(503, json={"detail": "Service unavailable"}),
httpx.Response(503, json={"detail": "Service unavailable"}),
httpx.Response(200, json=[{"id": 1}]),
]
calls = 0
def side_effect(_: httpx.Request) -> httpx.Response:
nonlocal calls
resp = responses[min(calls, len(responses) - 1)]
calls += 1
return resp
with respx.mock:
respx.get(f"{BASE_URL}/users").mock(side_effect=side_effect)
# First call — 503
with pytest.raises(httpx.HTTPStatusError):
client.list_users()
# ─────────────────────────────────────────────────────────────────────────────
# 4. Regex and pattern matching
# ─────────────────────────────────────────────────────────────────────────────
class TestPatternMatching:
def test_regex_url_matching(self, client: UserApiClient) -> None:
with respx.mock:
respx.get(url__regex=rf"{BASE_URL}/users/\d+").mock(
return_value=httpx.Response(200, json={"id": 42, "email": "[email protected]"})
)
result = client.get_user(42)
assert result["id"] == 42
# ─────────────────────────────────────────────────────────────────────────────
# 5. Async client testing
# ─────────────────────────────────────────────────────────────────────────────
class TestAsyncClient:
@pytest.mark.asyncio
async def test_async_list_users(
self, async_client: AsyncUserApiClient, mock_users: list[dict],
) -> None:
async with respx.mock:
respx.get(f"{BASE_URL}/users").mock(
return_value=httpx.Response(200, json=mock_users)
)
result = await async_client.list_users()
assert len(result) == 2
@pytest.mark.asyncio
async def test_async_create_user(self, async_client: AsyncUserApiClient) -> None:
created = {"id": 5, "email": "[email protected]", "role": "user"}
with respx.mock:
route = respx.post(f"{BASE_URL}/users").mock(
return_value=httpx.Response(201, json=created)
)
result = await async_client.create_user({"email": "[email protected]"})
assert result["id"] == 5
sent = json.loads(route.calls.last.request.content)
assert sent["email"] == "[email protected]"
# ─────────────────────────────────────────────────────────────────────────────
# 6. @respx.mock decorator
# ─────────────────────────────────────────────────────────────────────────────
@respx.mock
def test_delete_user_with_decorator(respx_mock: respx.MockRouter) -> None:
client = UserApiClient()
route = respx_mock.delete(f"{BASE_URL}/users/1").mock(
return_value=httpx.Response(204)
)
client.delete_user(1)
assert route.called
assert route.call_count == 1
For the responses library alternative — responses patches requests.get/post/... and only works with the synchronous requests library, while respx is purpose-built for httpx — both its sync httpx.Client and async httpx.AsyncClient — so async with respx.mock: result = await client.get(url) works without any extra patching, and route.calls.last.request exposes the full httpx.Request object (URL, headers, content, stream) rather than a simplified mock call record. For the pytest-httpx alternative — pytest-httpx is an official httpx test plugin providing a httpx_mock pytest fixture, while respx works as both a context manager and a decorator independent of pytest — useful when mocking outside of test functions or in production circuit-breaker logic — and respx’s url__regex and url__startswith matchers handle path-param routes more concisely than pytest-httpx’s match_ parameters. The Claude Skills 360 bundle includes respx skill sets covering respx.mock context manager and decorator, respx.get/post/put/patch/delete route registration, url__regex and url__startswith pattern matching, httpx.Response with JSON headers and status codes, side_effect for timeouts/errors/dynamic responses, sequential response cycling, route.called/call_count/calls.last.request for assertions, async with respx.mock for AsyncClient, Router for reusable mock sets, and pytest fixture integration. Start with the free tier to try httpx mocking code generation.