Files
2026-06-01 21:42:05 +00:00

139 lines
4.7 KiB
Markdown

# 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
1. Use `TestClient` or HTTPX ASGI clients for route tests.
2. Put shared app, client, and session setup in fixtures.
3. Use `app.dependency_overrides` to replace request-time collaborators.
4. Use `yield` fixtures when cleanup matters.
5. 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
```python
@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
```python
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
```python
@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
```python
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-29` defines a route whose collaborators come from dependency injection.
- `tests/test_dependency_security_overrides.py:44-63` overrides those collaborators with `app.dependency_overrides` and then resets the override map.
### Starlette
- `tests/test_applications.py:160-163` uses a yielded `client` fixture around a test client context manager.
- `tests/test_applications.py:234-238` uses `raise_server_exceptions=False` so tests can inspect a 500 response instead of immediately re-raising the server exception.
- `tests/test_applications.py:394-409` verifies lifespan startup before `yield` and cleanup after `yield` through the client context.
- `starlette/applications.py:46-48` explicitly documents lifespan as the preferred startup and shutdown mechanism.
### HTTPX
- `httpx/_transports/asgi.py:63-83` exposes `ASGITransport` specifically for driving an ASGI app in-process, including testing-oriented constructor options.
- `httpx/_transports/asgi.py:78-81` documents `raise_app_exceptions=False` for inspecting application failures as responses.
- `httpx/_transports/mock.py:15-43` exposes `MockTransport` as an explicit transport seam when testing HTTP clients.
### Full-stack FastAPI Template
- `backend/tests/conftest.py:15-24` keeps DB setup and cleanup explicit in a yielded session fixture.
- `backend/tests/conftest.py:27-30` defines a `TestClient` fixture with a visible context boundary.
- `backend/tests/conftest.py:33-42` derives 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.