respx, TestClient, dependency_overrides, and when the mock needs to be a real URLHonest 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.
respx only patches httpx โ requests and urllib sail past itWe 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.
TestClient mocks nothing external, and dependency_overrides is for your dependenciesThree 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.
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.
# 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.
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.
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 world | Mockbird | Notes |
|---|---|---|
respx.get(url).mock(...) | 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 |
Your app's /openapi.json | import it โ seeded mock of your schema | FastAPI generates the spec; we host the mock |
httpx.Response(500) stubs | ?mock_status=500 | on any URL, no re-registration |
side_effect=ConnectTimeout | ?mock_delay=3000 / ?mock_jitter | real time passes โ your timeout= genuinely fires (verified) |
| (no flakiness simulation) | ?mock_chaos=0.5 | real 5xx/429s for your retry logic to absorb |
dependency_overrides | env-var base URL | keep overrides for swapping your services; point the base URL at the mock for everything with a network hop |
Ordered side_effect lists | snapshot pinning | named data states, safe across parallel workers |
respx.calls assertions | request inspector | last 50 requests with method, path, query, headers, body |
respx.route(...).pass_through() | 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).
respx for millisecond unit tests of httpx-based code, and dependency_overrides for swapping your own services. Point uvicorn, workers, frontend fetches, requests-based SDKs and Playwright runs at a Mockbird base URL via settings. App code doesn't change.pass_through() 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.respx + TestClient/dependency_overrides | Mockbird | |
|---|---|---|
| What it is | pip-installable library + framework test features | free hosted service |
| Reachable from | httpx calls in the test process only | anything with HTTP: browser, Playwright, curl, requests, SDKs, workers, mobile, CI, teammates |
| requests / urllib / aiohttp under respx | not intercepted (verified) | it's a real URL โ every client works by definition |
| Unmatched request | AllMockedAssertionError for httpx 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 (measured ~34 ms) | ?mock_delay/?mock_jitter/?mock_chaos, real time passes |
| Works offline | yes | no โ it's a real network call |
| Request assertions | respx.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 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 โ