139 lines
4.7 KiB
Markdown
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.
|