ARTICLE · 1256791
接口工具 + AI 文档生成
前天我在整理一份接口文档,写着写着发现一个问题——文档里的金额类型是数字,接口要的却是字符串。
那份文档是我自己写的,写完那天是对的。后来后端把金额从 double 改成了 varchar,文档没跟着改。这个错误在文档里躺了两个月,直到有人照着文档写代码被接口拒了。
那天的排查过程挺浪费的:先是怀疑自己参数写错,再怀疑接口有 bug,最后才想起来「文档是旧的」。
今天想把这件事变成能自动跑出来的检查,顺便说说接口文档到底能干什么、不能干什么。
配套脚本在
examples/lab/day27/,五个文件都能直接跑。接口文档就在/openapi.json,用 requests 就能读,不需要装任何工具。
📄 先看看接口文档长什么样
很多同事说「接口文档是给人看的网页」。其实大部分接口都有一份机器也能读的版本,叫 OpenAPI(以前叫 Swagger)。规范见 OpenAPI 官方规范。
读它只要一行:
import requests
doc = requests.get("http://127.0.0.1:8765/openapi.json", timeout=5).json()
print(list(doc.keys()))
输出:
['openapi', 'info', 'paths']
就是个 JSON 字典。 最有用的是 paths——每个路径对应一个接口:
POST /orders 创建订单
GET /orders/{order_id} 查询订单
POST /orders/{order_id}/cancel 取消订单
这行清单的代码,套到任何有 OpenAPI 文档的服务上都能用:
for path, methods in doc["paths"].items():
for method, info in methods.items():
print(method.upper(), path, info.get("summary", ""))
「这个服务有哪些接口」从此不是靠问人,也不是靠翻文档,是靠跑一行代码。
🔍 再看一个接口的细节
创建订单这个接口,文档里写了字段和类型:
schema = doc["paths"]["/orders"]["post"]["requestBody"]["content"]["application/json"]["schema"]
print(schema["required"]) # ['amount']
print(schema["properties"]) # {'user_id': {'type': 'string'}, 'amount': {'type': 'number'}}
amount 的类型是 number。
记住这个值。下一课我们拿它跟实际接口对一对。
⚠️ 照着文档发一次,被拒了
文档说 amount 是数字,那就发数字:
response = requests.post("http://127.0.0.1:8765/orders", json={"amount": 199.8}, timeout=5)
print(response.status_code) # 400
print(response.json()) # {'error': 'amount_must_be_string', 'received_type': 'float'}
被拒了。接口要字符串。
换成字符串:
response = requests.post("http://127.0.0.1:8765/orders", json={"amount": "199.80"}, timeout=5)
print(response.status_code) # 201
成了。
文档说的那个类型,服务根本不收。
这种事有个名字,叫文档漂移——文档写完那天是对的,代码后来改了,文档没跟上。
🤖 把它变成一个能重复跑的检查
一次手工试出来不算本事,得能天天跑:
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},接口不收")
逻辑是这样的:请求是照着文档造的,那如果被拒了,错的只能是文档——不是你参数写错。
为什么值得每天都跑? 文档漂移的可怕之处在于它平时不出事。写文档那天是对的,代码一改就错了,而没人会主动去对。等到真有人照着文档写代码被拒了,第一反应通常是「这人不会用」,而不是「文档错了」。
挂在 CI 上,比写在 wiki 里提醒大家看有用得多。CI 的基本概念见 GitHub Actions 官方文档。
📋 从文档生成一批用例骨架
有了清单和字段说明,就能批量造用例:
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,针对单个资源的操作是 200
"预期状态码": 201 if (method == "POST" and "{" not in path) else 200,
"前置": "(文档没写)",
"预期结果": "(文档没写)",
})
return 用例
生成出来三条:
TC_POST__orders POST /orders
请求 :{"amount": 199.8} ← 注意,是数字
预期码:201
TC_GET__orders_order_id GET /orders/{order_id}
请求 :{}
预期码:200
TC_POST__orders_order_id_cancel POST /orders/{order_id}/cancel
请求 :{}
预期码:200
「预期码」那行我改过一次。 第一版我写的是「POST 一律 201」,结果取消订单那条实际返回 200,是我自己的测试把它抓出来的。
只有往集合里加东西才是 201,针对某个具体资源的操作(取消订单)都是 200。这条不在文档里,是从实际行为对出来的。
💥 让机器跑一遍,第一条就挂了
TC_POST__orders 预期 201 实际 400 不符
实际响应:{'error': 'amount_must_be_string', 'received_type': 'float'}
TC_GET__orders_order_id 预期 200 实际 200 符合
TC_POST__orders_order_id_cancel 预期 200 实际 200 符合
3 条里,2 条符合,1 条不符。
挂掉的那条,恰恰是前面那个文档漂移。
机器是照着文档造的数据(amount 传了数字),文档说 number,接口要 string。
所以结论是:按文档生成的用例,第一条就跑不通。修法不是改用例,是改文档。
但更值得看的是它没覆盖什么
数一数这套骨架覆盖了什么:
每个接口各一条 字段类型按文档填对
没覆盖的:
金额传错类型会怎样 订单不存在会怎样 已支付的订单取消会怎样 重复取消会怎样 谁有权限取消别人的订单
这五类,生成一条都生成不出来。
因为它们不在文档里。文档只说了「正常长什么样」,没说「不正常的时候该怎么反应」。而接口测试真正值钱的就是后半截。
生成能省掉「抄一遍参数」的活,省不掉「想清楚不正常时会怎样」。
✍️ 让 AI 起草反例,然后逐条核对
那五类交给 AI 想。让它起草,它给了五条:
AI 写的用例:金额传成数字
AI 写的预期:返回 400 参数错误
AI 写的用例:查询不存在的订单
AI 写的预期:返回 404
AI 写的用例:取消订单
AI 写的预期:返回 200,状态变 CANCELLED
AI 写的用例:重复取消订单
AI 写的预期:返回 200,状态还是 CANCELLED
AI 写的用例:别人能不能取消我的订单
AI 写的预期:返回 403 权限不足
看起来挺像那么回事。 逐条拿去实跑:
第 1 条 —— 实际 400。AI 猜对了。但你事先并不知道它会返回 400 还是 422,这条得跑一次才知道。
第 2 条 —— 实际 404。AI 猜对了。
第 3 条 —— 实际 200。AI 猜的没错,但它没说要再查一次。响应说成功,订单到底变没变?这是 Day26 那个四层断言。
第 4 条 —— 实际 200。AI 的预期不完整。
它说「返回 200,状态还是 CANCELLED」,但没说这次其实什么都没改。接口返回里有个 changed 字段,第二次是 False。
照它写断言,重复取消和首次取消会长得一模一样。
第 5 条 —— 实际 200。这条最危险。
AI 写「返回 403 权限不足」,听起来非常合理。但这个服务根本不校验身份,它返回 200。
照 AI 的预期写断言,这条会永远红——而你很可能会以为是接口有 bug。
更麻烦的是:403 这个预期是谁定的? 业务上「别人能不能取消我的订单」是条规则,可这条规则没人写过。AI 猜了一个最常见的答案,你照抄了,就等于把一个自己都不知道的规则写进了测试。
📝 所以 AI 起草之后要做三件事
预期里没有的观察点,要补上(第 3、4 条) 靠推理得出的状态码,要实跑一次确认(第 5 条) 文档里没有的业务规则,不能让 AI 替你定(第 5 条)
一句话:AI 能帮你想「该测哪些」,但不能帮你定「什么算对」。
📝 三个练习
答案在 05_answer.py,也可以跑 python -m pytest . -q 看结果。
练习一:列出接口清单
从文档里列出所有接口的「方法 + 路径 + 说明」。
答案:3 个,GET /orders/{order_id} 查询订单、POST /orders 创建订单、POST /orders/{order_id}/cancel 取消订单。
练习二:把漂移检查写成 pytest
def test_文档类型与实际一致(接口文档, 本地服务):
文档类型 = (接口文档["paths"]["/orders"]["post"]["requestBody"]
["content"]["application/json"]["schema"]["properties"]["amount"]["type"])
assert 文档类型 == "number"
响应 = requests.post(f"{本地服务}/orders", json={"amount": 199.8}, timeout=5)
assert 响应.status_code == 400
assert 响应.json()["error"] == "amount_must_be_string"
注意最后那个断言的用法:它不是在测「接口应该拒绝数字」,它是在测「文档说的和实际对不上」。这个区别很重要——文档修好之后,这个测试就该红了(因为漂移没了)。所以真正要挂到 CI 上的是反过来的一版。
练习三:给 AI 起草的用例补观察点
「重复取消」那条,AI 写的预期要补上 changed 和「再查一次」。
答案:
assert 第一次["changed"] is True
assert 第二次["changed"] is False
assert 再查["status"] == "CANCELLED"
💬 写在最后
今天两件事,一件是我踩过的坑,一件是省下来的活。
踩坑那件:文档会过期,而且没人提醒你。 靠人工对是比不过来的,得让它变成一个每次改动都会跑的检查。
省活那件:从文档生成用例骨架是真的快。 但你要清楚它只干了「抄参数」这一件事。
真正花时间的是后半截——哪些不正常的情况值得测、测到了算过还是算不过。 这部分文档给不了,AI 也给不了。
顺带说一句,Day12 我写过「想监控服务状态,Python 又太重」。今天这套东西——读文档、生成用例、跑检查——全部是标准库加一个 requests,一个下午就能搭起来,不用等公司买工具。
📢 评论区聊聊
你手上有没有过「文档和实际对不上」的坑?当时是怎么发现的? 你平时查一个服务有哪些接口,是问人、翻文档,还是有别的办法?
文档和接口的自动比对思路,可以参考 Prisma 的契约测试思路 里的做法。
👤 关于楠风
5 年测试工程师,在上海做测试平台方向,Web / 接口 / Android / PC 四端都是自己搭的。这个系列是边学边写,把踩过的坑记下来。
📅 下期预告
Day28《接口自动化 + AI 智能断言》
今天我们把断言写死了几个字段。明天我们把断言本身也做成能配的东西,并且用一份故意坏的响应,证明这些断言真的能抓住错。
🌟 点赞 + 在看 + 转发
如果你手上有一份接口文档,今天花两分钟照着发一次请求。
说不定你会发现,文档已经过期两个月了。
点赞是对今天这份耐心,在看是让更多人少踩一次坑。在看和转发不用花钱。















































































