โ† All guides

Mock APIs in Flask โ€” responses, requests-mock, test_client, and when the mock needs to be a real URL

Honest 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.

The three boundaries that send people searching

1. responses and requests-mock only patch requests โ€” urllib, httpx and aiohttp sail past them

We 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.

2. test_client() mocks nothing external

app.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.

3. No in-process mock survives the process boundary

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.

The hosted half: a mock API on 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..." }

Error paths without registering a single stub

# your view's 5xx handling, on demand:
requests.get(f"{API}/products", params={"mock_status": 503})     # real HTTP 503 (verified)

Timeouts where real time passes

with pytest.raises(requests.exceptions.Timeout):
    requests.get(f"{API}/products/1",
                 params={"mock_delay": 3000}, timeout=1)          # genuinely raises (verified)

Retry logic against a deterministic failure sequence

?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.

State: scripted answers vs. an actual store

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 concepts โ†’ Mockbird

Flask testing worldMockbirdNotes
responses.get(url, json=...)a resource on a real URLlist/get/create/update/delete generated, plus filtering, sorting, pagination, relations
Hand-rolled fixture dictsseeded realistic recordsfaker-style names/emails/prices/dates; or import your exact records from db.json/CSV/OpenAPI
responses.get(url, status=503)?mock_status=503on any URL, no re-registration
body=ReadTimeout() stubs?mock_delay=3000 / ?mock_jitterreal time passes โ€” your timeout= genuinely fires (verified)
Ordered stub lists for retries?mock_seq=503,503,200deterministic sequence, mock_seq_key per worker (verified recovery)
(no flakiness simulation)?mock_chaos=0.5real 5xx/429s for your retry logic to absorb
App-config upstream URL patchingenv-var base URLpoint UPSTREAM_URL at the mock; app code doesn't change
responses.calls assertionsrequest inspectorlast 50 requests with method, path, query, headers, body
responses.add_passthru(...)n/akeep using it for the hosts you don't mock

Try it in 10 seconds (shared demo, no setup)

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).

Or use both โ€” they compose

Honest comparison

responses / requests-mock + test_clientMockbird
What it ispip-installable libraries + Flask's test featuresfree hosted service
Reachable fromrequests calls in the test process onlyanything with HTTP: browser, Playwright, curl, urllib, httpx, SDKs, workers, mobile, CI, teammates
urllib / httpx / aiohttp under responsesnot intercepted (verified)it's a real URL โ€” every client works by definition
Unmatched requestloud ConnectionError for requests calls; invisible for other clientsn/a โ€” real endpoints answer real queries
Setupregister stubs per testone curl or one click; no code
Stateful CRUDno โ€” scripted answers onlydefault โ€” writes persist
Latency/retry/timeout testinginstant raises โ€” durations never run?mock_delay/?mock_seq/?mock_chaos, real time passes
Works offlineyesno โ€” it's a real network call
Request assertionsresponses.calls, precise, in-testrequest inspector (last 50, headers/body)
Request capnone10,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 โ†’