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