You need to test code that calls a third-party REST API through the requests library — including its retry logic, custom headers, and error handling — without the test ever touching the network. The responses library patches requests at the transport-adapter layer, so your real Session runs while canned responses are replayed and every call is recorded for assertion. This guide covers the registration API (@responses.activate, responses.add), ordered and unordered registries, matchers for query strings and bodies, and the assert_all_requests_are_fired guard that turns a forgotten stub into a failing test instead of silent dead code.
Prerequisites
responses >= 0.25(theregistriesandresponses.matchersmodules referenced here are stable from 0.21+;OrderedRegistrylives inresponses.registries).requests >= 2.31andpytest >= 8.0.responsesintercepts onlyrequests; forhttpxuserespxinstead, as covered in Mocking Network and HTTP Calls.
Solution
import requests
import responses
from responses import matchers
# Code under test: a thin client with one retry on 429.
def fetch_page(session: requests.Session, page: int) -> dict:
for _ in range(2): # one retry
resp = session.get(
"https://api.example.com/items",
params={"page": page},
headers={"Accept": "application/json"},
)
if resp.status_code == 429:
continue # retry once on rate-limit
resp.raise_for_status()
return resp.json()
resp.raise_for_status()
return resp.json()
@responses.activate # patches requests for this test only
def test_fetch_page_retries_then_succeeds():
# First registration -> 429; second registration -> 200. Same method+URL,
# consumed in order, so call 1 gets the 429 and call 2 gets the 200.
responses.add(
responses.GET,
"https://api.example.com/items",
json={"error": "rate_limit"},
status=429,
match=[matchers.query_param_matcher({"page": "2"})], # only fire for page=2
)
responses.add(
responses.GET,
"https://api.example.com/items",
json={"items": [1, 2, 3]},
status=200,
match=[matchers.query_param_matcher({"page": "2"})],
)
with requests.Session() as session: # the REAL client
result = fetch_page(session, page=2)
assert result == {"items": [1, 2, 3]}
# responses records every intercepted call in order.
assert len(responses.calls) == 2
assert responses.calls[0].response.status_code == 429
assert responses.calls[1].response.status_code == 200
# The query string and header reached the transport unchanged.
assert responses.calls[1].request.params == {"page": "2"}
assert responses.calls[1].request.headers["Accept"] == "application/json"
For finer control over consumption order, use an explicit registry:
import responses
from responses import registries
# OrderedRegistry forces responses to be consumed strictly in registration
# order and errors if a request arrives out of sequence.
@responses.activate(registry=registries.OrderedRegistry)
def test_strict_ordering():
responses.add(responses.GET, "https://api.example.com/a", json={"step": 1})
responses.add(responses.GET, "https://api.example.com/b", json={"step": 2})
import requests
assert requests.get("https://api.example.com/a").json()["step"] == 1
assert requests.get("https://api.example.com/b").json()["step"] == 2
The context-manager form makes the firing guard explicit:
import responses
def test_context_manager_guard():
# assert_all_requests_are_fired defaults to True here: an unused
# registration fails the test on context exit.
with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps:
rsps.add(responses.GET, "https://api.example.com/used", json={"ok": True})
import requests
requests.get("https://api.example.com/used")
# If you added an unused stub, the block would raise AssertionError on exit.
Why this works
responses registers a custom HTTPAdapter on the requests Session machinery, so requests never reach urllib3's connection pool or a socket. Because interception happens below your client code, the real Session, parameter encoding, header injection, and Response.json() decoding all execute exactly as in production — the only thing replaced is the bytes coming back off the wire. Registrations for the same method and URL form an ordered queue that is consumed one per matching request, which is what makes retry and pagination sequences testable, and assert_all_requests_are_fired inverts the check so a stub you forgot to exercise becomes a loud failure rather than silent coverage rot.
Edge cases and failure modes
- Trailing slash and query mismatches.
https://api.example.com/itemsand.../items/are different URLs, and a query string the code adds but the stub omits will not match. Usematchers.query_param_matcher(orquery_string_matcher) rather than baking params into the URL. - Unconsumed registrations. With
assert_all_requests_are_fired=True, a leftover stub fails the test. Keep it on to catch dead branches; turn it off only for deliberately optional fallbacks. - Queue exhaustion. Once all registrations for a URL are consumed, the last one repeats for further calls. If you expect an error after N calls, register exactly N responses with an
OrderedRegistryso an extra call raises instead of silently replaying. - Passthrough leakage.
responses.add_passthru(prefix)lets specific hosts reach the real network — combine it withpytest-socketallow-hosts so the rest of the suite stays blocked, as described in Mocking Network and HTTP Calls. - Wrong patch surface for non-requests clients.
responsesdoes nothing forhttpx,aiohttp, or rawurllib. Getting the interception layer right is the same discipline as choosing a patch target.
Frequently Asked Questions
What does assert_all_requests_are_fired do in responses?
When True it fails the test if any registered response was never matched by a request, catching dead stubs and skipped code paths. It defaults to True for the RequestsMock context manager and can be toggled per block.
How do I match a request by query string or JSON body with responses?
Pass matchers from responses.matchers to responses.add, for example matchers.query_param_matcher({'page': '2'}) or matchers.json_params_matcher({'id': 7}). A registration only fires when every supplied matcher passes against the incoming request.
Can responses return a different response on each call to the same URL?
Yes. Register the same method and URL multiple times; responses consumes registrations in order, so the first call gets the first registration and so on. The last registration repeats once the queue is exhausted unless you use an OrderedRegistry.
← Back to Mocking Network and HTTP Calls