API Tests: TestClient vs HTTPX
Scenario
You're testing a FastAPI app using `TestClient`. The test passes, but in production, the exact same endpoint raises a `RuntimeError: Timeout context manager should be used inside a task`.
Mental model
Testing a server requires a client. `TestClient` is a synchronous caller that bypasses the network. `httpx.AsyncClient` + ASGI transport actually executes the request in a true asynchronous event loop, perfectly mimicking production.
Deep dive
FastAPI's `TestClient` (built on `requests` and `starlette`) is synchronous. It's great for simple apps, but it runs your async endpoints in a simulated synchronous thread pool.
If your app uses `asyncio` deeply (like async database drivers or HTTP clients), `TestClient` can hide concurrency bugs. In these cases, you must use `httpx.AsyncClient` with an `ASGITransport` to test asynchronously.
Code examples
Sync TestClient
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
This is simple but entirely synchronous. If the endpoint `await`s something that relies on a specific event loop state, it might behave differently here than in prod.
AsyncClient (Ground Truth)
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.mark.asyncio
async def test_async_endpoint():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/")
assert response.status_code == 200
This runs the FastAPI app entirely asynchronously, ensuring the event loop behavior matches the real `uvicorn` production server.
Common mistakes
- Using TestClient for fully async apps: If your app uses `asyncpg` or `httpx` internally, `TestClient` can mask deadlocks or trigger event-loop mismatch errors. Always use `AsyncClient` for async apps.
Recall questions
- What library is `FastAPI(TestClient)` built on top of?
- Why is `httpx.AsyncClient` preferred over `TestClient` for heavily async applications?
- What transport is needed for `httpx.AsyncClient` to directly test a FastAPI app without a real network server?
Questions & answers
How do you structure your pytest fixtures to provide an `AsyncClient`?
I would create an `async def` fixture using `pytest-asyncio` that yields an `httpx.AsyncClient` configured with `ASGITransport(app=app)` so every test gets an isolated async client.
A test passes with `TestClient` but the app fails in production under uvicorn. What's the likely difference?
`TestClient` runs your app synchronously. If your app has blocking I/O inside an `async def` endpoint, `TestClient` might survive it, but uvicorn will freeze the event loop.
Continue learning
Previous: Async Tests
Previous: Pytest Basics and Fixtures