Chapter 09 · 测试:在上线前发现问题
📚 学完本章,你将能够:
- 解释为什么"手动在浏览器点一点"不是可靠的测试方法
- 用
TestClient编写自动化 API 测试 - 用
dependency_overrides替换真实依赖为 Mock - 为数据库测试创建隔离环境,保证测试互不干扰
- 运行测试套件并查看覆盖率报告
💡 场景引入
你重构了图书 API 的数据库查询逻辑,自信满满地部署上线。十分钟后,用户反馈"登录后看不到自己的书了"。
原来你在重构时不小心改了一个字段名,但手动测试时只测了"未登录的列表接口",漏掉了"登录后的个人接口"。人总是会漏掉边界情况,自动化测试不会。
9.1 为什么需要自动化测试
| 手动测试 | 自动化测试 |
|---|---|
| 每次改代码都要手动点一遍 | 运行一条命令,全部自动验证 |
| 容易遗漏边界情况 | 每个边界都写成用例,不会忘记 |
| 无法验证"没坏" | 重构后跑一次,确保旧功能正常 |
| 多人协作时重复劳动 | CI 自动跑,没过不让合并代码 |
9.2 TestClient:你的自动化浏览器
FastAPI 提供 TestClient,它在内存中模拟 HTTP 请求,不需要真的启动服务器:
基础测试
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_get_books():
response = client.get("/books")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_create_book():
response = client.post("/books", json={
"title": "测试书",
"author": "测试作者",
"price": 29.9
})
assert response.status_code == 201
assert response.json()["title"] == "测试书"
运行测试:
bash
pip install pytest
pytest tests/ -v
9.3 依赖覆盖:测试的杀手锏
测试认证接口时,你真的要生成 JWT、查数据库吗?不需要。dependency_overrides 让你替换任何依赖:
替换认证依赖
async def mock_current_user():
return User(id=1, username="tester", role="admin")
# 测试中临时替换
app.dependency_overrides[get_current_user] = mock_current_user
@router.delete("/books/{book_id}")
async def delete_book(..., user: User = Depends(get_current_user)):
...
def test_delete_book():
response = client.delete("/books/1")
assert response.status_code == 204
# 测试完清理
app.dependency_overrides.clear()
Mock 不是作弊
单元测试的目标是验证"当前函数的逻辑",而不是验证"JWT 库是否正确"。JWT 库由它的作者测试,你只需要验证自己的代码路径。
9.4 数据库隔离:每个测试从零开始
测试不应该污染真实数据库。标准做法是:为测试创建独立的数据库,每个测试在事务中运行,结束后回滚。
conftest.py(pytest 配置)
import pytest_asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from models import Base
TEST_DB_URL = "postgresql+asyncpg://user:pass@localhost/test_db"
engine = create_async_engine(TEST_DB_URL)
@pytest_asyncio.fixture(scope="session", autouse=True)
async def setup_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest_asyncio.fixture
async def db_session():
async with AsyncSession(engine) as session:
async with session.begin():
yield session
# 事务自动回滚,测试数据不残留
9.5 覆盖率:你测了多少代码?
运行覆盖率
pip install pytest-cov
pytest --cov=app --cov-report=html
# 然后打开 htmlcov/index.html 查看报告
覆盖率告诉你"哪些代码行被测试执行过"。但注意:高覆盖率不等于高质量。100% 覆盖率但断言很少,仍然是不可靠的测试。
🎯 知识检测
问题 1
TestClient 发送的请求会真的走网络吗?
解析:TestClient 基于 httpx 的 ASGI 传输,直接在 Python 内存中调用 FastAPI 应用,不经过 TCP/HTTP 网络栈。这意味着测试很快,且不需要服务器在运行。
问题 2
为什么要用
dependency_overrides 替换 get_current_user?
解析:单元测试的目标是验证当前接口的业务逻辑,而不是验证 JWT 解析。通过替换依赖,测试可以专注于"给定一个已登录用户,删除操作是否正确",而不关心 Token 怎么生成。
问题 3
覆盖率 100% 意味着?
解析:覆盖率只统计"哪些代码被跑过",不统计"这些代码的断言是否充分"。100% 覆盖率但缺少断言的测试,仍然可能放过 bug。覆盖率是必要指标,但不是充分指标。
本章小结
✓
自动化测试是安全网
重构和加功能时,测试确保旧代码没坏。
✓
TestClient 在内存中测试
不需要启动服务器,速度快,适合 CI。
✓
dependency_overrides 隔离依赖
替换认证、数据库等外部依赖,专注测试当前逻辑。
✓
事务回滚实现数据隔离
每个测试后自动回滚,测试互不干扰。