responses, requests-mock, test_client, and when the mock needs to be a real URLHonest split first: responses is the right default for fast offline unit tests of Flask code that calls external APIs through the requests library โ which is most Flask code. It patches requests' adapter layer, its default for an unmatched call is a loud ConnectionError (not silent passthrough), and requests-mock gives you the same coverage as a pytest fixture if you prefer that style. This page is not going to pretend otherwise.
One scoping note before anything else: Flask's test_client() exercises your app โ it has nothing to do with mocking the external APIs your views call. That confusion sends a lot of people searching, so it gets its own verified section below.
Everything below was verified the week of writing (Aug 2026: Flask 3.1.3, requests 2.34, responses 0.26, requests-mock 1.12, urllib3 2.7, Python 3.12, pytest 9) โ every snippet on this page was actually run, in a 10-test suite that passes.
responses and requests-mock only patch requests โ urllib, httpx and aiohttp sail past themWe registered a responses stub for a URL and then fetched that same URL two ways in the same test:
@responses.activate(assert_all_requests_are_fired=False)
def test_boundary():
responses.get(f"{API}/products/1", json={"id": 1, "name": "stub"})
requests.get(f"{API}/products/1").json()["name"] # "stub" โ intercepted โ
with urllib.request.urlopen(f"{API}/products/1") as f:
json.load(f)["name"] # the REAL record โ urllib
# hit the live network mid-test
Both libraries work by mounting a fake transport adapter on requests sessions. Anything that isn't requests โ urllib.request, httpx, aiohttp, a subprocess running curl โ goes straight to the real network. If part of your stack uses httpx (or an SDK does, internally), your "fully mocked" test is quietly doing live calls.
test_client() mocks nothing externalapp.test_client() routes fake HTTP into your Flask app in-process. It's the right tool for testing your views โ but when a view calls requests.get(upstream), that call is as real as in production. test_client + no interception library = your test suite is hammering the actual third-party API (or failing in CI where there's no network). You need both: test_client to drive your app, and something to answer your app's outbound calls.
We started a real Flask server (separate process) whose view proxies an upstream via requests, then activated responses in the test process with a stub for that exact upstream URL โ and called the running server:
responses.get(f"{API}/products/1", json={"name": "stub-should-not-appear"})
proc = subprocess.Popen([sys.executable, "upstream_server.py"]) # flask run, own process
requests.get("http://127.0.0.1:5111/first-product").json()
# โ the REAL upstream record. The server never saw the stub. (verified)
responses patches Python objects in this interpreter. A running flask run dev server, a gunicorn worker, a Celery task, a cron job, your React frontend's fetch, a Playwright browser, a teammate's laptop โ none of them can be reached by it. The moment the thing that makes the call lives outside your pytest process, the mock has to be a real URL.
Mockbird gives you a stateful mock REST API at https://mockbird.mockbird.workers.dev/m/<project> โ one curl to create, seeded with realistic data, CORS open, no signup. Your Flask app just points its upstream base URL at it (an env var you already have), and every client โ requests, urllib, the running server, the browser โ sees the same mock, because it's not a mock of a client library, it's an actual API.
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'content-type: application/json' \
-d '{"preset":"ecommerce"}'
# โ { "id": "abc123...", "adminKey": "...", "baseUrl": ".../m/abc123..." }
# your view's 5xx handling, on demand:
requests.get(f"{API}/products", params={"mock_status": 503}) # real HTTP 503 (verified)
with pytest.raises(requests.exceptions.Timeout):
requests.get(f"{API}/products/1",
params={"mock_delay": 3000}, timeout=1) # genuinely raises (verified)
?mock_seq=503,503,200 makes the endpoint answer 503, 503, then real data โ perfect for urllib3's Retry:
retry = Retry(total=3, status_forcelist=[503], backoff_factor=0, allowed_methods=["GET"])
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=retry))
r = s.get(f"{API}/products/1", params={"mock_seq": "503,503,200",
"mock_seq_key": "worker1"})
assert r.status_code == 200 # recovered (verified)
mock_seq_key isolates parallel pytest-xdist workers. More recipes in testing loading & error states.
A responses stub is a canned script. A mocked POST "creates" nothing โ the next GET returns whatever you scripted, because there is no store behind it. Against Mockbird the write is real:
o = requests.post(f"{API}/orders", json={"status": "pending", "total": 12.5}).json()
requests.get(f"{API}/orders/{o['id']}").json()["total"] # 12.5 โ actually there
requests.delete(f"{API}/orders/{o['id']}") # cleanup is real too
requests.get(f"{API}/orders/{o['id']}").status_code # 404 (verified)
For deterministic test data across runs (and parallel workers), save a named snapshot and pin it per-request with the X-Mockbird-Snapshot header โ deterministic test data guide.
| Flask testing world | Mockbird | Notes |
|---|---|---|
responses.get(url, json=...) | a resource on a real URL | list/get/create/update/delete generated, plus filtering, sorting, pagination, relations |
| Hand-rolled fixture dicts | seeded realistic records | faker-style names/emails/prices/dates; or import your exact records from db.json/CSV/OpenAPI |
responses.get(url, status=503) | ?mock_status=503 | on any URL, no re-registration |
body=ReadTimeout() stubs | ?mock_delay=3000 / ?mock_jitter | real time passes โ your timeout= genuinely fires (verified) |
| Ordered stub lists for retries | ?mock_seq=503,503,200 | deterministic sequence, mock_seq_key per worker (verified recovery) |
| (no flakiness simulation) | ?mock_chaos=0.5 | real 5xx/429s for your retry logic to absorb |
| App-config upstream URL patching | env-var base URL | point UPSTREAM_URL at the mock; app code doesn't change |
responses.calls assertions | request inspector | last 50 requests with method, path, query, headers, body |
responses.add_passthru(...) | n/a | keep using it for the hosts you don't mock |
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=2&select=name,price'
curl -i 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=503'
curl 'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=2000'
All against the shared demo project (resets daily).
responses/requests-mock for millisecond unit tests of requests-based code. Point the running dev server, workers, frontend fetches, urllib/httpx callers and Playwright runs at a Mockbird base URL via config. App code doesn't change.add_passthru() the hosted mock for everything else.GET /m/<project>/db.json ejects your entire dataset any time; openapi.json and postman.json are generated from your live schema.responses / requests-mock + test_client | Mockbird | |
|---|---|---|
| What it is | pip-installable libraries + Flask's test features | free hosted service |
| Reachable from | requests calls in the test process only | anything with HTTP: browser, Playwright, curl, urllib, httpx, SDKs, workers, mobile, CI, teammates |
| urllib / httpx / aiohttp under responses | not intercepted (verified) | it's a real URL โ every client works by definition |
| Unmatched request | loud ConnectionError for requests calls; invisible for other clients | n/a โ real endpoints answer real queries |
| Setup | register stubs per test | one curl or one click; no code |
| Stateful CRUD | no โ scripted answers only | default โ writes persist |
| Latency/retry/timeout testing | instant raises โ durations never run | ?mock_delay/?mock_seq/?mock_chaos, real time passes |
| Works offline | yes | no โ it's a real network call |
| Request assertions | responses.calls, precise, in-test | request inspector (last 50, headers/body) |
| Request cap | none | 10,000/project/day |
Written by the Mockbird maker โ bias disclosed. Where responses and Flask's own tools genuinely win: they run offline at zero latency, the unmatched-call default is safely loud, requests-mock's fixture style fits pytest beautifully, and responses.calls assertions are more precise than any log-based check. For fast unit tests of requests-based code they should stay your default. When the thing you need is a URL โ for a running server, a Celery worker, an httpx caller, a frontend fetch, Playwright, a teammate, or CI against a deployed preview โ that's us.
Full API reference in the docs. More guides: mock API for Python ยท mock APIs in FastAPI ยท mock APIs in Django ยท mock server from OpenAPI ยท deterministic test data ยท testing loading & error states. Create your API โ