文档漂移是最贵的一类接口问题:写文档那天是对的,代码改完就错了,而没人主动去对。 直到有人照文档写代码被拒,第一反应还是「这人不会用」。
今天把这件事变成一个能重复跑的检查。
配套脚本在
examples/lab/day27/,五个文件都能直接跑,接口文档就在/openapi.json,不需要装任何工具。
💸 一次手工排查的代价
那份文档是我自己写的,写完那天是对的。后来后端把金额从 double 改成 varchar,文档没跟着改。
这个错误在文档里躺了两个月。
暴露它的那天,排查路径是这样的:
- 照文档发请求 → 400
- 怀疑自己参数写错 → 改了三种写法,还是 400
- 怀疑接口有 bug → 翻服务端日志,没报错
- 最后才想起来「文档是旧的」
第 4 步之前的所有时间,都是因为没人把「文档说的」和「实际做的」放一起对过。
🔍 对的方式:照文档造一个值发过去
python
def 按文档试一次(base, doc, path, method, field):
schema = doc["paths"][path][method]["requestBody"]["content"]["application/json"]["schema"]
doc_type = schema["properties"][field]["type"]
样例 = {
"string": lambda: "199.80",
"number": lambda: 199.8,
"integer": lambda: 200,
"boolean": lambda: True,
}
value = 样例[doc_type]()
response = requests.post(f"{base}{path}", json={field: value}, timeout=5)
return doc_type, value, response.status_code
doc_type, value, status = 按文档试一次(base, doc, "/orders", "post", "amount")
if status >= 400:
print(f"文档和实现对不上:文档说 {doc_type},接口不收")
这个检查的巧妙之处在于归因。 OpenAPI 规范见 官方文档,常见状态码语义见 MDN 参考,requests 用法见 官方快速上手。
请求是照着文档造出来的,所以它被拒这件事,错的只能是文档------不可能是你的参数写错,因为你是严格照着文档写的。
实测结果:文档说 number,发过去 400 amount_must_be_string;发字符串 201。
🔁 为什么值得天天跑
文档漂移的阴险之处在于它平时不出事。
- 写文档那天是对的
- 代码一改,文档就错了
- 没有任何人会主动去对,除非有人刚好踩到
而踩到的人会付出上面那 4 步的时间。更糟的是,他会把结论归到「接口难用」或者「自己不会」,不会想到文档。
所以这个检查的正确位置是 CI:
text
python -m pytest examples/lab/day27 -q
人不会去看 wiki,但 CI 会拦人。
📋 从文档生成用例:快的部分和慢的部分
生成本身很快,三十行代码:
python
def 从文档造用例(doc):
用例 = []
for path, methods in doc["paths"].items():
for method, info in methods.items():
method = method.upper()
body = info.get("requestBody")
if body:
schema = body["content"]["application/json"]["schema"]
必填 = schema.get("required", [])
字段 = schema.get("properties", {})
else:
必填, 字段 = [], {}
用例.append({
"编号": f"TC_{method}_{path.replace('/', '_').replace('{','').replace('}','')}",
"方法": method,
"路径": path,
"请求": {k: 按类型造值(v["type"]) for k, v in 字段.items() if k in 必填},
"预期状态码": 201 if (method == "POST" and "{" not in path) else 200,
"前置": "(文档没写)",
"预期结果": "(文档没写)",
})
return 用例
它省掉的是「抄一遍参数」。 三个接口三十行代码 vs 手写三遍路径和字段,这个省下的时间是实打实的。
但它有个前提问题:它只能抄文档里有的东西。
跑一遍你会发现,生成的第一条直接挂了:
text
TC_POST__orders 预期 201 实际 400 不符
TC_GET__orders_order_id 预期 200 实际 200 符合
TC_POST__orders_order_id_cancel 预期 200 实际 200 符合
因为它照文档造了 {"amount": 199.8},而文档是错的。
修法不是改用例,是改文档。
生成的骨架覆盖不到什么
- 金额传错类型会怎样
- 订单不存在会怎样
- 已支付的订单取消会怎样
- 重复取消会怎样
- 谁有权限取消别人的订单
这五类生成一条都生成不出来。 文档只说了「正常长什么样」,没说「不正常的时候该怎么反应」。
生成能省掉抄参数的活,省不掉想清楚不正常时会怎样。 后者才是接口测试真正值钱的部分。
🤖 AI 起草反例:能省,但省不了判断
AI 给五条反例,其中一条特别值得说:
| AI 写的用例 | AI 写的预期 | 实际 |
|---|---|---|
| 别人能不能取消我的订单 | 返回 403 权限不足 | 200 |
这个服务根本不校验身份。 照 AI 的预期写断言,这条会永远红------而你会以为是接口有 bug。
问题不在于 AI 猜错了状态码,在于**「403 权限不足」这个预期本身就没有来源**。「别人能不能取消我的订单」是条业务规则,可这条规则没人写过。AI 猜了一个最常见的答案,你照抄,就等于把一个自己都不知道的规则写进了测试。
另外三条的毛病轻一些但同样真实:
- 「取消订单」------没说要再查一次(Day26 那个四层断言)
- 「重复取消订单」------没说这次其实什么都没改 (
changed=False) - 「金额传成数字」------猜对了 400,但事先不知道是 400 还是 422,得跑一次才知道
📌 落到工程上的三条
一、漂移检查挂 CI,不挂 wiki。 一个 pytest 用例加一条命令的事。
二、从文档生成骨架可以自动化,但它只是起点。 生成的用例跑一遍就发现了文档漂移------这本身就值了。
三、AI 起草之后必须实跑确认。 特别是不靠文档、靠推理得出的状态码。
🗂️ 一个能把生成用例真正用起来的目录结构
text
lab/
├── stub_api.py 教学服务(也是后面几天的公共依赖)
├── day27/
│ ├── conftest.py fixture:本地服务、接口文档
│ ├── 01_read_docs.py 读文档、列清单
│ ├── 02_doc_drift.py 漂移检查
│ ├── 03_generate_cases.py 从文档生成骨架
│ ├── 04_ai_draft.py AI 起草 + 逐条核对
│ └── 05_answer.py 练习答案
服务放最外层,因为后面六天都要用它。 Day28 要用它测断言,Day29 要用它做 Mock,Day32 要拿它跑配置化用例。
每天一个独立目录。 你 cd 进去跑,不用记得先 cd 到哪。
fixture 单独一份。 pytest 会自动加载 conftest.py,里面放的 fixture 不用 import 就能用。
为什么这么摆? 因为接口测试最容易烂在「改一个东西,十几个文件都要翻」。把服务、fixture、测试分开之后,改断言不动服务,改服务不动断言。
🧮 一个跨天的数字
成功支付总额一直是 359.60,被七种不同的实现算出来过:SQL 的 SUM、纯函数的 for 循环、类的组合、标准库的 Counter、requests 加 Pydantic、pytest 的断言。
每次换技术栈都重新算一遍,数字一样。
为什么较这个劲? 因为「过滤条件被顺手改掉」是这类代码最常见的错误,而它不会让任何测试变红------测试和新代码是一起改的。只有一个来自别处、来自另一种技术实现的数字,才能发现。
📝 三个练习
- 列出接口清单 ------ 3 个,方法和说明都要对。
- 把漂移检查写成 pytest ------ 文档说是
number,实跑被拒 400,错误码amount_must_be_string。 - 给 AI 起草的用例补观察点 ------
changed分别 True 和 False,最终状态 CANCELLED。
第 2 题有个容易搞混的地方:它测的不是「接口应该拒绝数字」,是「文档和实际对不上」。 文档修好之后这个测试就该红了(漂移没了)。真正要挂 CI 的是反过来的一版。
🔭 最后
文档漂移是接口测试里最贵的一类问题,因为它平时完全隐形。
从文档生成用例是真的快,但你要清楚它只干了「抄参数」这一件事。真正花时间的是后半截------哪些不正常的情况值得测、测到了算过还是算不过。
这部分文档给不了,AI 也给不了。
下一步是 Day28《接口自动化 + AI 智能断言》,我们把断言写死的字段变成能配的东西,并且用一份故意坏的响应,证明这些断言真的能抓住错。