โ† All guides

Mock APIs in FastAPI โ€” respx, TestClient, dependency_overrides, and when the mock needs to be a real URL

Honest split first: respx is the right default for fast offline unit tests of FastAPI code that calls external APIs through httpx โ€” which is most FastAPI code. It patches httpx's transport layer, works for sync and async clients alike, and its default for an unmatched httpx call is a loud AllMockedAssertionError, not silent passthrough. This page is not going to pretend otherwise.

One scoping note before anything else: FastAPI's TestClient and dependency_overrides exercise and rewire your app โ€” they have nothing to do with mocking the external APIs your endpoints 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: FastAPI 0.141, Starlette 1.3, Python 3.12, respx 0.23, httpx 0.28, responses 0.26, uvicorn 0.52, pytest 9) โ€” every snippet on this page was actually run, in a 12-test suite that passes.

The three boundaries that send people searching

1. respx only patches httpx โ€” requests and urllib sail past it

We registered a respx mock for a URL and then fetched that same URL three ways:

@respx.mock
def test_boundary():
    respx.get(f"{API}/products/1").mock(
        return_value=httpx.Response(200, json={"id": 1, "name": "FAKE"}))

    httpx.get(f"{API}/products/1").json()      # {"name": "FAKE"}  โœ… mocked
    requests.get(f"{API}/products/1").json()   # real record       โŒ real network
    urllib.request.urlopen(...)                # real record       โŒ real network

The requests and urllib calls returned live data over the network, mid-test โ€” the mock never saw them. Any SDK that ships its own requests/aiohttp client, any legacy urllib call, any subprocess: not covered. (The responses library is the same story mirrored โ€” we verified it patches requests only while httpx walks past it. Each mock library covers exactly one client library.)

Credit where due: respx's default for an unmatched httpx call is a raised AllMockedAssertionError ("<Request> not mocked!") โ€” loud and safe, verified. But it can't raise for clients it never intercepts.

2. TestClient mocks nothing external, and dependency_overrides is for your dependencies

Three verified facts that untangle the most-confused corner of FastAPI testing:

# 1) TestClient alone: your endpoint's external call is REAL
client = TestClient(app)
client.get("/price-sync/1").json()   # real upstream record came back (verified)

# 2) dependency_overrides swaps YOUR service object โ€” great, but in-process only
app.dependency_overrides[get_catalog] = lambda: FakeCatalog

# 3) the good case: respx DOES reach app code under TestClient,
#    because TestClient runs your app in the same process
with respx.mock:
    respx.get(f"{API}/products/1").mock(...)
    client.get("/price/1")           # async endpoint's httpx call: mocked โœ…

TestClient is an in-process caller of your ASGI app โ€” it intercepts nothing your app sends outward. dependency_overrides is FastAPI's own, genuinely good mechanism for swapping a service โ€” but the override lives on the app object in the test process. Neither exists anywhere a real server is running.

3. The mock never leaves your test process

respx, dependency_overrides and mock.patch all mutate state inside the Python process running the test. A uvicorn server is a different process. So is a worker consuming a queue, a scheduled job, and โ€” always โ€” the browser: your frontend's own fetch() calls never touch Python at all. Playwright driving a real dev server gets nothing from your mock.

We proved it the direct way: an endpoint that calls the external API with httpx, served by real uvicorn; the test process activated a respx mock for that exact upstream URL and then hit the endpoint like a browser would:

@respx.mock
def test_uvicorn_ignores_my_mock():
    respx.get(f"{API}/products/1").mock(json={"name": "FAKE"})
    httpx.get(f"{API}/products/1").json()["name"]     # "FAKE" โ€” works HERE

    # โ€ฆbut the same endpoint under `uvicorn app.main:app` (separate process):
    json.load(urllib.request.urlopen("http://127.0.0.1:8908/price-sync/1"))
    # โ†’ real upstream record โ€” the mock never existed there (verified)

No amount of fixture engineering fixes any of these three โ€” the mock has to live somewhere every process, every client library and every browser can reach: a URL.

The 60-second version

# one curl, no signup โ€” a whole seeded e-commerce backend
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
  -H 'content-type: application/json' -d '{"name":"shop","preset":"ecommerce"}'
# โ†’ {"id":"abc123","adminKey":"KEY", ...}   โ† save both

curl https://mockbird.mockbird.workers.dev/m/abc123/products?limit=3
# โ†’ 3 seeded products, CORS on, writes persist

Then wire it the way FastAPI already wants external services wired โ€” settings plus an env var:

# settings via pydantic-settings (or plain os.environ)
class Settings(BaseSettings):
    catalog_api_url: str = "https://api.yourapp.com"

# services.py
async def price(pid: int) -> dict:
    async with httpx.AsyncClient(timeout=5) as client:
        r = await client.get(f"{settings.catalog_api_url}/products/{pid}")
        return r.json()
# .env for dev / CI job env
CATALOG_API_URL=https://mockbird.mockbird.workers.dev/m/abc123

Now uvicorn, the worker, the frontend's fetch, the requests-based SDK, Playwright and your terminal all see the same mock โ€” because it's just a URL. Prefer clicking? This link creates the same project in your browser โ€” no account. And since FastAPI generates OpenAPI for free: paste your app's /openapi.json at /app#import and get a seeded mock of your own schema.

Error, latency and chaos states your mocks can't exercise honestly

With respx, a "timeout" is side_effect=httpx.ConnectTimeout โ€” it raises without real time passing. We measured it: ~34 ms, so the timeout=5 you passed was never exercised; the duration logic in your code never runs. Against a hosted URL, real time passes โ€” all three of these are from the verified suite:

# a real 503, no handler to write
httpx.get(f"{API}/products", params={"mock_status": 503})            # โ†’ 503

# a real timeout: response held for 3s, timeout=1 genuinely fires after ~1.07s
httpx.get(f"{API}/products", params={"mock_delay": 3000}, timeout=1)
# โ†’ httpx.ReadTimeout

# retry logic against a genuinely flaky endpoint:
r = get_with_retries(client, f"{API}/products", {"mock_chaos": 0.5})
r.status_code   # โ†’ 200 โ€” five runs in the suite, five 200s after
                #   real retries absorbed real 5xx/429s

More recipes in testing loading & error states.

State: scripted answers vs. an actual store

A respx 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:

r = httpx.post(f"{API}/products", json={"name": "Widget", "price": 9.99})
pid = r.json()["id"]                                   # 201, real id
httpx.get(f"{API}/products/{pid}").json()["name"]      # "Widget" โ€” actually there
httpx.delete(f"{API}/products/{pid}")                  # cleanup is real too

For deterministic test data across runs (and parallel pytest-xdist workers), save a named snapshot and pin it per-request with the X-Mockbird-Snapshot header โ€” deterministic test data guide.

FastAPI testing concepts โ†’ Mockbird

FastAPI testing worldMockbirdNotes
respx.get(url).mock(...)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
Your app's /openapi.jsonimport it โ†’ seeded mock of your schemaFastAPI generates the spec; we host the mock
httpx.Response(500) stubs?mock_status=500on any URL, no re-registration
side_effect=ConnectTimeout?mock_delay=3000 / ?mock_jitterreal time passes โ€” your timeout= genuinely fires (verified)
(no flakiness simulation)?mock_chaos=0.5real 5xx/429s for your retry logic to absorb
dependency_overridesenv-var base URLkeep overrides for swapping your services; point the base URL at the mock for everything with a network hop
Ordered side_effect listssnapshot pinningnamed data states, safe across parallel workers
respx.calls assertionsrequest inspectorlast 50 requests with method, path, query, headers, body
respx.route(...).pass_through()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

respx + TestClient/dependency_overridesMockbird
What it ispip-installable library + framework test featuresfree hosted service
Reachable fromhttpx calls in the test process onlyanything with HTTP: browser, Playwright, curl, requests, SDKs, workers, mobile, CI, teammates
requests / urllib / aiohttp under respxnot intercepted (verified)it's a real URL โ€” every client works by definition
Unmatched requestAllMockedAssertionError for httpx 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 (measured ~34 ms)?mock_delay/?mock_jitter/?mock_chaos, real time passes
Works offlineyesno โ€” it's a real network call
Request assertionsrespx.calls, precise, in-testrequest inspector (last 50, headers/body)
Request capnone10,000/project/day

Written by the Mockbird maker โ€” bias disclosed. Where respx and FastAPI's own tools genuinely win: they run offline at zero latency, respx covers sync and async httpx alike and fails loudly on unmatched calls, dependency_overrides is the cleanest dependency-swap mechanism in any Python framework, and respx.calls assertions are more precise than any log-based check. For fast unit tests of httpx-based code they should stay your default. When the thing you need is a URL โ€” for uvicorn, a worker, a requests-based SDK, 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 Django ยท mock APIs in Flask ยท mock server from OpenAPI ยท mock APIs in Playwright ยท deterministic test data ยท testing loading & error states. Create your API โ†’