FastAPI 接口怎么测才靠谱?pytest、fixture 与依赖覆盖
摘要:用 TestClient 从 HTTP 契约测试 FastAPI,通过 fixture 隔离每个测试的状态,用 dependency_overrides 替换仓库,并用参数化覆盖输入边界。

图:测试客户端发出真实请求,fixture 重置状态,依赖覆盖把外部资源替换成可控测试实现。
Swagger 适合探索接口,却无法保证明天改完代码后所有行为仍然正确。更糟的是,有些测试单独运行通过,放进全套测试就失败,原因往往是它们偷偷共享了全局状态。
这篇文章不追求“测到每一行”,而是建立一套可以长期运行的最小结构:每个测试得到全新的 Repository,正常路径和失败路径都按 HTTP 响应断言,修改依赖时不会碰真实数据库。
先看一组能重复运行的测试
python -m pytest tests/test_ch12.py -vv
预期能看到三个独立参数化 case。重复运行两次,结果应完全相同,说明测试没有依赖上一次运行遗留状态。
先看清应用为测试留下了哪个替换点
cd 04-fastapi-beginner
source .venv/bin/activate
python -m pytest --version
阅读 可测试应用 和 测试文件。应用依赖 TaskRepository 协议,而不是直接访问全局列表。
用 fixture 和依赖覆盖隔离状态
建立替换边界
class TaskRepository(Protocol):
def add(self, payload: TaskCreate) -> TaskRead: ...
def list(self) -> list[TaskRead]: ...
def get_repository() -> TaskRepository:
return default_repository
路由只依赖能力接口,生产实现和测试实现都可注入。
为每个测试创建 fixture
@pytest.fixture
def client():
repository = InMemoryTaskRepository()
app.dependency_overrides[get_repository] = lambda: repository
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
每个测试得到新 Repository,finally 类的清理发生在 yield 之后。
参数化非法输入
@pytest.mark.parametrize(
"payload",
[{}, {"title": ""}, {"title": "x" * 101}],
)
def test_invalid_task_payloads_return_422(client, payload):
assert client.post("/tasks", json=payload).status_code == 422
新增边界只需增加一条数据,不复制整个测试函数。

图:每个测试拥有独立状态,运行结束后重置,并通过依赖覆盖隔离外部实现。
连续运行、筛选运行,再故意改坏契约
python -m pytest tests/test_ch12.py -q
python -m pytest tests/test_ch12.py -vv
python -m pytest tests/test_ch12.py -k invalid -q
预期最后一条只运行非法输入测试。故意把 max_length=100 改成 200,确认 101 字符测试能够发现契约变化,再恢复代码。
高价值测试盯住调用者可观察行为
高价值接口测试验证调用者可观察行为:状态码、响应 JSON、关键响应头、持久化副作用和权限边界。不要把实现细节的每一行都 mock 掉,否则重构会产生大量无意义失败。
测试必须独立、可重复、快速。独立指不依赖顺序,可重复指同样输入得到同样结果,快速让开发者愿意频繁运行。
fixture 的作用域要谨慎。数据库测试通常函数级最安全;会话级虽然快,但需要可靠回滚或清理策略。

图:高价值测试盯住调用者能观察到的结果,而不是绑定内部实现细节。
测试数量很多,不代表测试可靠
- 全套测试共享可变列表:单独运行通过,合并运行失败。
- 只断言状态码:响应字段错误仍可能漏过。
- 测试结束不清理 override:污染后续测试。
- 把第三方服务真实请求放进单元测试:速度慢且受网络波动影响。
- 为追求覆盖率写无行为价值的断言:覆盖率是线索,不是质量目标。
把边界条件变成参数化数据
- 增加重复标题规则及其 409 测试。
- 新增 fixture 创建一条默认任务。
- 参数化 limit 的 0、1、100、101 四个边界。
- 随机顺序运行或手工交换测试顺序,确认结果不变。
- 为 JWT 登录示例补充重复注册和过期令牌测试。
最后,用测试固定这次改动
手动请求通过后,运行与本文对应的自动化测试:
python -m pytest tests/test_ch12.py -q
python tests/validate_course.py
写在最后
自动化测试已经成为后续改造的安全网。下一篇进入应用级请求处理:用 lifespan 管理启动资源,用 middleware 添加观测信息,用 CORS 明确允许的浏览器来源。