第 20 章 · 测试 FastAPI 应用
本章目标:用
TestClient+ pytest 为 FastAPI 应用编写自动化测试,掌握依赖覆盖、生命周期测试与 WebSocket/文件上传测试。
20.1 TestClient 基础
FastAPI 基于 Starlette 的 TestClient(底层是 HTTPX),不需要真正启动服务器、不占端口,直接在进程内调用 ASGI 应用:
uv add httpx pytest # TestClient 需要 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 # 与 main.py 同包时的相对导入
client = TestClient(app) # 把 app 传给 TestClient
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}运行 pytest 即可。三个关键约定:
- 测试函数用普通
def,不是async def;调用客户端也不用await——这样可以直接配合 pytest,无需额外插件; TestClient用法与 httpx 完全一致:路径/查询参数拼进 URL,JSON 体传给json参数,表单用data,请求头用headers,Cookie 用cookies;- 发送的数据是"能转 JSON 的对象",不是 Pydantic 模型;如有模型先用
jsonable_encoder转换。
TIP
也可以 from starlette.testclient import TestClient,fastapi.testclient 只是同一对象的便捷出口。
20.2 用 pytest fixture 组织测试
真实项目的 app 可能需要 lifespan 初始化资源。把 client 放进 fixture,并用 with 语句让 lifespan 在测试中触发:
import pytest
from fastapi.testclient import TestClient
from .main import app
@pytest.fixture
def client():
# with 语句进入时执行 startup(lifespan 前半段)
# 退出时执行 shutdown(lifespan 后半段),如关闭连接池
with TestClient(app) as c:
yield c
def test_health(client):
assert client.get("/health").status_code == 200不用 with 时 TestClient 不会跑 lifespan——这是"为什么测试里数据库没初始化"的最常见原因。
20.3 dependency_overrides:替换依赖
测试外部服务(付费认证接口、数据库)时不应该真的去调。FastAPI 提供了 app.dependency_overrides 字典:key 是原依赖函数,value 是替代函数,原依赖的子依赖也不会再执行:
# 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): # 模拟昂贵的第三方认证
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 user# test_main.py
from fastapi.testclient import TestClient
from fastapi import Depends
from .main import app, get_current_user
client = TestClient(app)
def fake_user():
return {"username": "test_user"} # 永远成功的 mock 用户
def test_me_with_override():
app.dependency_overrides[get_current_user] = fake_user
resp = client.get("/me")
assert resp.status_code == 200
assert resp.json() == {"username": "test_user"}
app.dependency_overrides.clear() # 清空恢复原始依赖要点:
- 无论原依赖用在路径函数参数、装饰器
dependencies=[...]还是include_router里,都能被覆盖; - 只想在个别测试中替换时,就在该测试开头设置 override、结尾
clear()或置为{}; - 这同样是替换测试数据库的标准手法:override 掉
get_db,返回内存 SQLite 会话。
20.4 测试 WebSocket 与文件上传
WebSocket 测试
同样使用 TestClient,通过 websocket_connect 建立会话上下文:
# main.py 中已有
@app.websocket("/ws")
async def ws(websocket):
await websocket.accept()
await websocket.send_text("hello")
msg = await websocket.receive_text()
await websocket.send_text(f"echo: {msg}")
await websocket.close()# test_ws.py
from fastapi.testclient import TestClient
from .main import app
def test_websocket_echo():
client = TestClient(app)
with client.websocket_connect("/ws") as ws:
assert ws.receive_text() == "hello"
ws.send_text("world")
assert ws.receive_text() == "echo: world"with 块内收发消息必须交替匹配服务端行为:先 receive_text() 收到 "hello",再发送、再接收回显。
上传文件与表单测试
httpx 风格:文件用 files 参数,表单字段用 data:
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
@app.post("/upload")
async def upload(file: UploadFile, note: str = Form(...)):
content = await file.read()
return {"filename": file.filename, "size": len(content), "note": note}def test_upload():
client = TestClient(app)
resp = client.post(
"/upload",
files={"file": ("report.txt", b"hello data", "text/plain")},
data={"note": "季度报表"},
)
assert resp.status_code == 200
assert resp.json()["size"] == 10
assert resp.json()["filename"] == "report.txt"20.5 本章小结
TestClient(app)进程内直连 ASGI 应用,测试函数用同步def,无需启动服务器;- 传参规则同 httpx:
json/data/headers/cookies/files;Pydantic 模型需先经jsonable_encoder; with TestClient(app)才会触发 lifespan 的 startup/shutdown,WebSocket 测试用with client.websocket_connect(...);app.dependency_overrides[原依赖] = 替代函数可在任意位置替换依赖(含子依赖不再执行),结束记得clear();- 替换数据库、跳过第三方认证都靠依赖覆盖实现。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 使用 TestClient 编写测试时,官方推荐的做法是?
2. 为什么测试中数据库连接池没有初始化?最可能的原因是?
3. 关于 app.dependency_overrides,说法正确的是?
4. 用 TestClient 测试 POST /upload 上传文件,正确写法是?
🛠️ 动手实践
- 为第 19 章拆分后的 todos 应用编写完整测试:fixture 创建 client + 内存 SQLite override,覆盖创建、查询、404 三条路径。
- 给应用加一个需要
X-API-Key头的 admin 接口,写两个测试:带正确 key 返回 200、错误 key 返回 403。 - 为第 17 章的聊天 WebSocket 写一个测试:连接后收到欢迎语,发送消息并断言广播回显内容。
应用能被可靠测试后,最后一章解决配置管理与上线部署:第 21 章 · 配置管理与应用生命周期。