到第三篇为止,我们已经有了:
分层架构
数据库接入
JWT/OAuth2鉴权

现在项目最容易出现的风险是:
你每次改一处功能,都不确定会不会把别的接口弄坏
这就是为什么第四步必须是测试体系建设

本文目标:
1. 用pytest与TestClient建立FastAPI的主流程测试
2. 让CRUD、鉴权、异常语义都可回归
3. 把测试接入CI,做到提交即验证


一、测试要测什么先划边界
 

建议优先级:
1. 接口主流程:成功路径200/201
2. 业务规则:冲突与非法输入400/409/422
3. 资源边界:不存在资源404
4. 鉴权边界:未登录/权限不足401/403

第一阶段就别追求100% 覆盖率,先保证关键路径可回归


二、依赖安装与基础约定
 

python -m pip install pytest pytest-cov httpx

约定建议:
- 测试目录:tests/
- 文件命名:test_*.py
- 函数命名:test_*


三、最小可用测试骨架
 

tests/conftest.py示意:

import pytest
from fastapi.testclient import TestClient
from app.main import app


@pytest.fixture
def client():
    return TestClient(app)

第一条烟雾测试:

tests/test_health.py

def test_health(client):
    resp = client.get("/health")
    assert resp.status_code == 200
    payload = resp.json()
    assert payload["success"] is True
    assert payload["data"]["status"] in ["ok", "健康"]


四、CRUD测试:按业务链路写,不按函数写
 

tests/test_users.py示意:

def test_user_crud_flow(client):
    # 创建
    create_resp = client.post("/users", json={"name": "Alice", "email": "alice@example.com"})
    assert create_resp.status_code == 201
    user_id = create_resp.json()["data"]["id"]

    # 查询
    get_resp = client.get(f"/users/{user_id}")
    assert get_resp.status_code == 200
    assert get_resp.json()["data"]["email"] == "alice@example.com"

    # 更新
    update_resp = client.put(f"/users/{user_id}", json={"name": "Alice2"})
    assert update_resp.status_code == 200
    assert update_resp.json()["data"]["name"] == "Alice2"

    # 删除
    delete_resp = client.delete(f"/users/{user_id}")
    assert delete_resp.status_code == 200

为什么这么写:
贴近真实用户行为
一条测试覆盖多段关键链路


五、异常语义测试:这是联调稳定性的关键
 

建议至少覆盖:
1. 参数错误 -> 422
2. 资源不存在 -> 404
3. 业务冲突邮箱重复-> 400/409

示例:

def test_create_user_validation_error(client):
    resp = client.post("/users", json={"name": "a", "email": "not-email"})
    assert resp.status_code == 422
    assert resp.json()["success"] is False


def test_get_not_found(client):
    resp = client.get("/users/99999")
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "HTTP_404"


def test_duplicate_email(client):
    client.post("/users", json={"name": "A", "email": "dup@example.com"})
    resp = client.post("/users", json={"name": "B", "email": "dup@example.com"})
    assert resp.status_code in [400, 409]
    assert resp.json()["success"] is False


六、鉴权测试:401/403必测
 

你应该至少有三类鉴权测试:
1. 未带token访问受保护接口 -> 401
2. 普通用户访问管理员接口 -> 403
3. 有效token访问 -> 200

示意:

def test_me_unauthorized(client):
    resp = client.get("/me")
    assert resp.status_code == 401


def test_admin_forbidden(client, user_token_headers):
    resp = client.delete("/admin/users/1", headers=user_token_headers)
    assert resp.status_code == 403


def test_me_authorized(client, user_token_headers):
    resp = client.get("/me", headers=user_token_headers)
    assert resp.status_code == 200


七、数据库测试策略:隔离、可重复、可回滚
 

常见做法:
测试环境使用独立数据库或测试库
每个测试前后清理数据
通过依赖覆盖override dependency注入测试Session

FastAPI常用依赖覆盖:

app.dependency_overrides[get_db] = get_test_db

完成后记得清理:

app.dependency_overrides.clear()


八、异步接口测试如果你用了async
 

对于异步场景,可用httpx.AsyncClient:

import pytest
from httpx import AsyncClient


@pytest.mark.asyncio
async def test_async_endpoint(app):
    async with AsyncClient(app=app, base_url="http://test") as ac:
        resp = await ac.get("/health")
    assert resp.status_code == 200

什么时候必须上异步测试:
你有大量async route
你要测试并发行为、超时行为、取消行为


九、覆盖率不是目的,但要有底线
 

建议先做到:
核心业务路径覆盖
鉴权边界覆盖
异常分支覆盖

运行:

pytest -q --cov=app --cov-report=term-missing

可设阶段目标:
先60%,再75%,再按模块逐步提升


十、把测试接进CI提交即回归
 

如果你用GitHub Actions,最小工作流示意:

name: test
on: [push, pull_request]
jobs:
  pytest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: pip install pytest pytest-cov httpx
      - run: pytest -q

收益:
坏提交会被第一时间拦截
团队协作成本下降


十一、常见坑与规避
 

1. 测试共享数据导致串扰
        解决:每个测试独立数据准备与清理

2. 只测成功路径,不测失败路径
        解决:422/404/401/403/业务冲突必须覆盖

3. 只在本地跑,不接CI
        解决:提交自动跑测试,避免我本地是好的

4. 对响应结构断言太弱
        解决:不仅断状态码,也断success/error/code/data


十二、本篇小结
 

这篇的核心不是会写pytest语法,而是把质量关口前移:
每次改动都可验证
异常语义可回归
鉴权边界可证明

下一篇是系列收官:
我们会做FastAPI的性能与部署优化,覆盖Uvicorn/Gunicorn、多环境配置、监控与日志治理
 

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐