API Tests: TestClient vs HTTPX

RoadmapsPython Backend

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

Recall questions

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

Return to Python Backend Roadmap