pytest — Fixtures, Conftest, and Async Testing
Set up pytest with fixtures, conftest.py, and async test support for FastAPI applications
The Problem
Testing async FastAPI apps needs async test support, a test client that speaks ASGI, and shared fixtures across test files. Standard unittest doesn’t support any of this natively.
Test Setup
conftest.py — Shared Fixtures
# tests/conftest.py
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
This fixture:
- Creates an
AsyncClientthat talks directly to your FastAPI app (no HTTP server needed) - Uses
ASGITransport— in-process, no network overhead yieldmakes it a generator fixture — cleanup happens after the test- Available to all test files (conftest.py is auto-discovered by pytest)
pytest Configuration
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-v --tb=short --cov=app --cov-report=term-missing"
asyncio_mode = "auto"— async test functions run automatically, no@pytest.mark.asyncioneeded--cov=app— measure coverage for theapp/package--cov-report=term-missing— show which lines aren’t covered
Running Tests
# Run all tests
pytest
# Run with verbose output
pytest -v
# Run a specific test class
pytest tests/test_api.py::TestItemsAPI
# Run a specific test
pytest tests/test_api.py::TestItemsAPI::test_create_item
Why httpx Over TestClient
FastAPI provides TestClient (based on requests), but it’s synchronous. For async code:
| Feature | TestClient (requests) | AsyncClient (httpx) |
|---|---|---|
| Async support | ❌ Wraps in sync | ✅ Native async |
| Event loop | Creates its own | Uses test’s event loop |
| In-process | ✅ | ✅ |
| API compatible | requests-like | requests-like |
httpx.AsyncClient with ASGITransport is the modern approach for testing async FastAPI apps.
Fixture Scope
The client fixture has default scope (function) — a new client per test. This ensures tests are isolated. For expensive setup (database connections), use broader scopes:
@pytest.fixture(scope="session")
async def db_engine():
engine = create_async_engine(TEST_DATABASE_URL)
yield engine
await engine.dispose()
Next Step
In the next lesson, we write unit tests for business logic — testing validators and service functions in isolation.