Chapter 20 · Testing FastAPI Applications
Chapter goals: Write automated tests for FastAPI apps with
TestClient+ pytest, master dependency overrides, lifespan testing, and WebSocket/file upload testing.
20.1 TestClient Basics
FastAPI is built on Starlette's TestClient (underlying HTTPX) — no real server startup needed, no port occupied, calls the ASGI app directly in-process:
uv add httpx pytest # TestClient requires httpx# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}# test_main.py
from fastapi.testclient import TestClient
from .main import app # Relative import when in the same package
client = TestClient(app) # Pass the app to TestClient
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}Run with pytest. Three key conventions:
- Test functions use plain
def, notasync def; noawaiton the client — this integrates directly with pytest without extra plugins; TestClientusage is identical to httpx: path/query params in the URL, JSON body viajsonparam, form data viadata, headers viaheaders, cookies viacookies;- Data sent must be "JSON-serializable objects", not Pydantic models; convert models with
jsonable_encoderfirst if needed.
TIP
You can also from starlette.testclient import TestClient; fastapi.testclient is just a convenience re-export of the same object.
20.2 Organizing Tests with pytest Fixtures
Real projects' apps may need lifespan-initialized resources. Put the client in a fixture and use with to trigger the lifespan during the test:
import pytest
from fastapi.testclient import TestClient
from .main import app
@pytest.fixture
def client():
# with statement triggers startup (first half of lifespan) when entering
# triggers shutdown (second half) when exiting — e.g., closing connection pools
with TestClient(app) as c:
yield c
def test_health(client):
assert client.get("/health").status_code == 200Without with, TestClient does not run the lifespan — this is the most common cause of "database not initialized in tests."
20.3 dependency_overrides: Replacing Dependencies
When testing external services (paid auth APIs, databases), you should not actually call them. FastAPI provides app.dependency_overrides dict: key is the original dependency function, value is the replacement — the original dependency's sub-dependencies also do not execute:
# main.py
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
username: str
async def verify_token_from_provider(token: str): # Simulate expensive third-party auth
raise HTTPException(status_code=401)
async def get_current_user(user=Depends(verify_token_from_provider)):
return user
@app.get("/me")
async def me(user: User = Depends(get_current_user)):
return userIn tests:
# test_main.py
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_me_with_mocked_auth():
# Override the auth dependency to bypass the real provider
async def fake_verify_token(token: str = "fake"):
return {"username": "testuser"}
app.dependency_overrides[verify_token_from_provider] = fake_verify_token
response = client.get("/me", headers={"Authorization": "Bearer fake"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up after the test
app.dependency_overrides.clear()20.4 Testing WebSocket and File Uploads
TestClient supports WebSocket and multipart form data natively:
def test_websocket(client: TestClient):
with client.websocket_connect("/ws") as ws:
ws.send_text("hello")
data = ws.receive_text()
assert data == "Echo: hello"
def test_file_upload(client: TestClient):
from io import BytesIO
file_content = b"file contents"
response = client.post(
"/upload/",
files={"file": ("test.txt", BytesIO(file_content), "text/plain")}
)
assert response.status_code == 20020.5 Chapter Summary
TestClient(app)runs the app in-process without a real server;- Use
with TestClient(app) as c:to trigger lifespan in tests; app.dependency_overridesreplaces dependencies for isolated testing;- WebSocket and file upload tests are supported natively.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Why does TestClient not require a real server?
2. Why use with TestClient(app) instead of client = TestClient(app)?
3. What does app.dependency_overrides do?
4. How do you test a WebSocket endpoint with TestClient?
🛠️ Hands-On Exercises
- Write tests for a CRUD app: create, read, update, delete — assert status codes and response bodies.
- Use
dependency_overridesto replace a real DB session with an in-memory dict; verify tests run without a database. - Write a WebSocket test that connects, sends a message, and asserts the echo response.
Proceed to Chapter 21: Configuration Management and Application Lifecycle.