4.7 KiB
Testing
Test FastAPI applications at the HTTP or ASGI boundary whenever practical. Prefer client-based tests, dependency overrides, and explicit lifespan or session fixtures over patching internals.
Why this convention exists
FastAPI does a lot of important work around your code:
- request parsing
- dependency injection
- auth wiring
- validation
- error serialization
- startup and shutdown behavior
If tests bypass those seams, they skip the behavior the framework is responsible for.
The repeated pattern across FastAPI, Starlette, HTTPX, and production templates is:
- drive the app through a client
- make fixture lifetime explicit
- override dependencies at the framework seam
- exercise lifespan behavior deliberately
The convention
- Use
TestClientor HTTPX ASGI clients for route tests. - Put shared app, client, and session setup in fixtures.
- Use
app.dependency_overridesto replace request-time collaborators. - Use
yieldfixtures when cleanup matters. - Enter lifespan-aware client contexts when startup or shutdown behavior is part of the test.
Follow it for boundary behavior
This is the default for:
- route tests
- auth and permission tests
- validation and error-contract tests
- startup and shutdown behavior
- request and response schema behavior
Pure business logic is different: if the code is framework-agnostic, test it directly as normal Python.
Preferred shapes
Client fixture with explicit lifetime
@pytest.fixture
def client() -> Generator[TestClient, None, None]:
with TestClient(app) as client:
yield client
This makes startup and teardown behavior visible.
Dependency overrides for request-time seams
app.dependency_overrides[get_current_user] = fake_user
try:
response = client.get("/items")
finally:
app.dependency_overrides = {}
This swaps collaborators where FastAPI expects them to be swapped.
Explicit DB or session cleanup fixture
@pytest.fixture(scope="session", autouse=True)
def db() -> Generator[Session, None, None]:
with Session(engine) as session:
init_db(session)
yield session
cleanup_db(session)
session.commit()
This makes resource lifetime and cleanup rules obvious.
Counterexamples
Monkeypatch soup
monkeypatch.setattr("app.api.routes.items.session", fake_session)
monkeypatch.setattr("app.api.routes.items.get_user", fake_user)
Bad because the test couples to route internals instead of public seams.
Testing startup-sensitive behavior without entering client context
If the app depends on startup-initialized resources, a client used without the proper lifespan context can produce misleading green tests.
Calling route functions directly for integration-style assertions
That skips request parsing, dependency injection, validation, and error serialization, which is most of what FastAPI is doing for you.
Source signals
FastAPI
tests/test_dependency_security_overrides.py:24-29defines a route whose collaborators come from dependency injection.tests/test_dependency_security_overrides.py:44-63overrides those collaborators withapp.dependency_overridesand then resets the override map.
Starlette
tests/test_applications.py:160-163uses a yieldedclientfixture around a test client context manager.tests/test_applications.py:234-238usesraise_server_exceptions=Falseso tests can inspect a 500 response instead of immediately re-raising the server exception.tests/test_applications.py:394-409verifies lifespan startup beforeyieldand cleanup afteryieldthrough the client context.starlette/applications.py:46-48explicitly documents lifespan as the preferred startup and shutdown mechanism.
HTTPX
httpx/_transports/asgi.py:63-83exposesASGITransportspecifically for driving an ASGI app in-process, including testing-oriented constructor options.httpx/_transports/asgi.py:78-81documentsraise_app_exceptions=Falsefor inspecting application failures as responses.httpx/_transports/mock.py:15-43exposesMockTransportas an explicit transport seam when testing HTTP clients.
Full-stack FastAPI Template
backend/tests/conftest.py:15-24keeps DB setup and cleanup explicit in a yielded session fixture.backend/tests/conftest.py:27-30defines aTestClientfixture with a visible context boundary.backend/tests/conftest.py:33-42derives auth fixtures from the client instead of bypassing the app boundary.
Bottom line
Test the app where FastAPI actually does work:
- through the client
- through dependencies
- through lifespan
- with explicit fixture lifetime
Patch internals only when there is no better seam.