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:污染后续测试。
  • 把第三方服务真实请求放进单元测试:速度慢且受网络波动影响。
  • 为追求覆盖率写无行为价值的断言:覆盖率是线索,不是质量目标。

把边界条件变成参数化数据

  1. 增加重复标题规则及其 409 测试。
  2. 新增 fixture 创建一条默认任务。
  3. 参数化 limit 的 0、1、100、101 四个边界。
  4. 随机顺序运行或手工交换测试顺序,确认结果不变。
  5. JWT 登录示例补充重复注册和过期令牌测试。

最后,用测试固定这次改动

手动请求通过后,运行与本文对应的自动化测试:

python -m pytest tests/test_ch12.py -q
python tests/validate_course.py

写在最后

自动化测试已经成为后续改造的安全网。下一篇进入应用级请求处理:用 lifespan 管理启动资源,用 middleware 添加观测信息,用 CORS 明确允许的浏览器来源。