接口与逻辑测试
接口与逻辑测试 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课给 P08-P16 攒下的接口装上安全网:用 pytest + TestClient 覆盖成功、输入错误、重复写入、外部服务失败四类场景,全程用临时数据库,绝不碰你真实的学习记录。
问题场景
P13 改过分页逻辑,你手动 curl 了六遍才敢重启服务;P15 改了错误分类,前端联调时才发现 cancelled 事件没发。这些验证每次都要人来点,人就会漏。测试的价值不是「显得专业」,是把六遍 curl 固化成毫秒级可重复的脚本。
核心解释
FastAPI 官方路径是 TestClient:不需要真的起 uvicorn,进程内直接调路由,连接口异常都能断言。
# conftest.py —— fixture:每个测试拿独立的临时库
import pytest
import sqlite3
from fastapi.testclient import TestClient
from main import app, get_db
@pytest.fixture
def client(tmp_path):
conn = sqlite3.connect(tmp_path / "test.db") # pytest 内置的临时目录
conn.row_factory = sqlite3.Row
init_schema(conn)
app.dependency_overrides[get_db] = lambda: conn
with TestClient(app) as c:
yield c
app.dependency_overrides.clear() # 测试完拆掉,防止污染下一个测试
# test_notes.py —— 四类场景
def test_create_success(client):
res = client.post("/notes", json={"title": "函数与模块", "body": "拆分心得"})
assert res.status_code == 201
assert res.json()["title"] == "函数与模块" # 断言可见行为,不只断言状态码
def test_create_invalid_input(client):
res = client.post("/notes", json={"title": "", "body": "x"})
assert res.status_code == 422
assert res.json()["error"]["category"] == "invalid_param" # P13 的统一错误结构
def test_create_duplicate(client):
client.post("/notes", json={"title": "A", "body": "x"})
res = client.post("/notes", json={"title": "A", "body": "y"})
assert res.status_code == 409
def test_upstream_failure(client, monkeypatch):
monkeypatch.setattr("main.fetch_one", fake_timeout) # 替换外部依赖
res = client.post("/ai/chat", json={"message": "hi"})
assert res.status_code == 200
assert "unreachable" in res.text # 上游失败也走 SSE error 事件
四条纪律对应四类场景之外的真实收益:
- fixture 管隔离。
tmp_path是 pytest 内置的临时目录(每测试唯一),配 P09 的dependency_overrides换掉数据库——测试从头到尾不碰reactlab.db,你的真实学习记录绝对安全。这条是硬红线。 - 断言行为不是断言实现。断言「返回 201 且 body 里标题正确」,而不是断言内部调了哪个函数;重构内部实现时测试不该跟着改。
- 外部依赖一律替换。P10 的超时、P15 的上游,用
monkeypatch换成假实现,测试才稳定可控——网络抖动不该决定测试红绿。 - 错误路径是测试的主体。422/409/上游失败的用例数量应该超过成功用例;成功路径只有一条,失败路径无穷多。
示例拆解
跑起来看输出:
pip install pytest httpx
pytest -v
# test_create_success PASSED
# test_create_invalid_input PASSED
# test_create_duplicate PASSED
# test_upstream_failure PASSED
# 4 passed in 0.3s
验证隔离是否真生效:故意在 conftest.py 里把 override 注释掉跑一次,测试会去连真库(或报错)——立刻改回来。这一步的意义是确认「测试不碰真实数据」不是嘴上说说,而是由 override 机制保证的。之后每次改接口先 pytest,绿了再联调,前端同学(就是你自己)会感谢这套红绿灯。
动手任务
- 给
/courses分页补测试:limit=500返回 422、page=999返回空 items 且 200——覆盖 P13 的两个边界。 - 用 monkeypatch 让上传解析必然失败,断言文档状态变
failed且 error 字段非空(P14 的失败留痕)。 - 故意删掉
dependency_overrides.clear(),连续跑两次 pytest 观察是否互相污染,然后加回来。
常见错误
- 测试连真实数据库:没有 override 的 TestClient 会按生产配置连库,学习数据被测试数据污染;conftest 里强制临时库。
- 只测成功路径:四个 200 覆盖不了任何真实风险;每写一个成功用例,配一个失败用例。
- 测试间共享状态:模块级列表当「数据库」,跑单测过跑全套挂;每个测试独立 fixture。
- 断言过深:断言内部函数被调用、断言 dict 完全相等,改个字段全红;断言外部可见行为。
检查与复盘
自查清单:
- pytest 全绿且耗时秒级,可以当作每次改代码后的习惯动作
- 确认测试运行期间真实
reactlab.db没有被写入 - 四类场景(成功/输入错/重复/上游失败)各有至少一个用例
参考资料
- FastAPI 官方教程:Testing —— TestClient 与依赖覆盖
- pytest 官方文档 —— fixture、monkeypatch 与参数化
读完这一课了?
标记完成后,工作台会安排接下来的学习。