โ† 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 server from OpenAPI ยท mock APIs in Playwright ยท deterministic test data ยท testing loading & error states. Create your API โ†’