Testing FastAPI Applications
FastAPI is easy to test. Its TestClient calls your application in-process, without a running server, and dependency overrides let you replace databases, authentication, and external services with test versions in one line. Combined with pytest fixtures, you can write fast unit-style endpoint tests and realistic integration tests against a real database.
A good suite covers the HTTP contract (status codes, validation errors, response shapes), business logic, authorization rules, and database behavior, while staying fast enough to run on every commit.
TL;DR
TestClient(app)makes synchronous requests to the ASGI app in-process; use it for most endpoint tests.- For async tests, use
httpx.AsyncClient(transport=ASGITransport(app=app))withpytest-asynciooranyio. app.dependency_overridesswaps dependencies (DB session, current user, clients) for fakes.- Test against a real database (Testcontainers, or a test database) with per-test transaction rollback for isolation.
- Mock outbound HTTP with respx or pytest-httpx; don't hit real third-party APIs.
- Use
with TestClient(app) as client:so lifespan startup and shutdown run.
Quick Example
Core Concepts
TestClient
fastapi.testclient.TestClient wraps the app with an httpx-compatible interface (client.get, post, put, and so on) that dispatches directly to the ASGI app. There's no network and no server, and it works with both sync and async endpoints. Using it as a context manager (with TestClient(app) as client) triggers the lifespan handler, so resources created at startup (DB engines, HTTP clients) exist during the test.
Async Tests
When tests themselves need to await (async fixtures, async DB sessions), use an async client:
ASGITransport doesn't run lifespan events by itself; use asgi-lifespan's LifespanManager, or set up resources in fixtures. Keep event loop scopes consistent between async fixtures and tests to avoid "attached to a different loop" errors, especially with async database drivers.
Dependency Overrides
app.dependency_overrides[original] = replacement replaces any dependency, including nested ones, for all requests until cleared. Typical overrides:
get_current_user→ a fixed test user, or a parametrized one for permission tests.get_session→ a session bound to a test transaction.- External clients (payments, email) → fakes that record calls.
Always clear overrides in fixture teardown. See FastAPI dependencies.
Database Testing
- Use the same database engine as production (PostgreSQL via Testcontainers, or a dedicated test database). SQLite differs in types, constraints, and SQL features.
- Isolation via rollback: open a connection, begin a transaction, bind the session to it, yield to the test, then roll back. Each test starts clean and no data persists.
- Migrations: run Alembic migrations once per test session to verify they work and create the schema.
- Factories:
factory_boyor simple helper functions create test data concisely.
See integration testing.
Testing Authentication and Authorization
- Override
get_current_userfor most tests, parametrizing roles and scopes. - Add a few tests exercising real token validation: valid token, expired token, wrong audience, and missing scopes.
- Test object-level authorization explicitly: user A must not access user B's resources. See FastAPI auth.
Mocking External Services
Outbound HTTP calls should be intercepted:
Or inject a fake client via dependency override, which is often cleaner than patching. See mocking and stubbing.
Best Practices
Test the Contract, Not the Framework
Focus on your behavior: status codes, validation messages, response shapes, authorization, and side effects. There's no need to test that FastAPI parses JSON.
Keep Business Logic in Plain Functions
Service functions that take explicit arguments are testable without HTTP at all. Endpoint tests then cover wiring and HTTP concerns, and service tests cover the rules.
Parametrize Permission Matrices
Use pytest.mark.parametrize over roles, scopes, and expected statuses to cover authorization systematically in few lines.
Run the Suite in CI With Real Services
Run Testcontainers or service containers (PostgreSQL, Redis) in CI, and include alembic upgrade head in the pipeline. See CI/CD.
Common Mistakes
Leaking Overrides Between Tests
Setting app.dependency_overrides at module level and never clearing it makes tests order-dependent. Set and clear overrides inside fixtures.
Testing Against SQLite While Production Uses PostgreSQL
JSON operators, ON CONFLICT, timezone handling, and constraint behavior differ, so tests pass while production fails. Use the real engine.
Forgetting Lifespan
Creating TestClient(app) without a with block skips startup, so resources initialized in lifespan are missing, and tests fail with confusing AttributeErrors or None clients.
FAQ
Do I need async tests for async endpoints?
No. TestClient can call async endpoints from synchronous tests. Use async tests (httpx AsyncClient) only when the test itself needs to await things, like async fixtures or direct async database queries.
How do I test endpoints that require authentication?
Override the get_current_user dependency with a function returning a test user, adjusting scopes and roles per test. Keep a few tests that send real tokens through the full validation path to cover the security code itself.
How do I isolate database state between tests?
Wrap each test in a transaction that's rolled back afterwards (binding the session to an outer connection with savepoints), or truncate tables between tests. Transaction rollback is faster, and keeps tests independent.
How do I test background tasks?
TestClient runs BackgroundTasks after the response within the same call, so their effects are visible after the request returns. For real task queues (Celery, Arq, Dramatiq), run tasks eagerly in tests, or assert on enqueued jobs.
Related Topics
- FastAPI — The framework overview
- pytest — The test runner and fixtures
- FastAPI Dependencies — Overrides for testing
- FastAPI Authentication — What to test in security
- Integration Testing — Real databases and services
- Mocking & Stubbing — Faking external APIs