Testing with httpx.AsyncClient and ASGI In-Memory Transport
- Last updated
- Prerequisites:
- pytest & pytest-asyncio basics
- FastAPI dependency overrides
- fastapi
- testing
- httpx
- asyncclient
- asgi
- pytest
- mocking
Read at your depth
The practical view
FastAPI recommends `httpx.AsyncClient` with `ASGITransport` for testing async endpoints with `pytest`. `async with AsyncClient(transport=ASGITransport(app=app), base_url='http://test') as ac: response = await ac.get('/users')`. This replaces the legacy synchronous `TestClient` for testing async endpoints, WebSockets, and lifespan events.
The same idea in other frameworks
`AsyncClient(transport=ASGITransport(app=app))` with `app.dependency_overrides`.`Test.createTestingModule()` with `supertest` in-memory testing.Spring Boot `@WebMvcTest` / `MockMvc` in-memory test runner.Supertest wrapping Express/Fastify apps.Legacy vs modern
Synchronous TestClient vs Modern AsyncClient with Dependency Overrides
Synchronous `TestClient` cannot run inside native async pytest loops; `AsyncClient` tests async endpoints with clean dependency overrides.
from fastapi.testclient import TestClient; client = TestClient(app) // ❌ Cannot await async fixturesasync with AsyncClient(transport=ASGITransport(app=app)) as ac: ... // ✅ NEW: Pure async testingInterview gotchas
Context
Interviewers test your knowledge of test transaction isolation.
The mechanical answer
Create a `pytest` async fixture that creates a nested database transaction (`connection.begin_nested()`) or runs a rollback transaction: execute the test, and in the fixture teardown, **call `transaction.rollback()`**. Override the `get_db` dependency using `app.dependency_overrides[get_db] = override_get_db`. All database queries execute against the real database schema inside an isolated transaction that is rolled back instantly when the test finishes.
Trap
Always call `app.dependency_overrides.clear()` in test cleanup fixtures to prevent test pollution across different test files.
Context
Checks awareness of HTTPX 0.27+ breaking API updates.
The mechanical answer
In HTTPX 0.27+, passing `AsyncClient(app=app)` was deprecated and removed. You must explicitly instantiate an **`ASGITransport`**: `AsyncClient(transport=ASGITransport(app=app), base_url='http://test')` to clearly separate transport layer mechanics from client request options.
Trap
Passing `app=app` directly in modern HTTPX will throw a `TypeError: unexpected keyword argument 'app'`.