接口 Mock + AI 自动生成数据
有个接口,金额传 "199.8"(少一位小数)、传 "-19.80"(负数)、传 "abc"——全被收下,状态码 201。
因为它的校验只有一行:判断这是不是个字符串。
这个漏洞是 Mock 发现的。真实环境里,要等到有人真传了负数金额、月底对账对不上,才会有人来查。
配套脚本在
examples/lab/day29/,四个文件都能直接跑。代码在 Windows、Python 3.11 下实跑,pytest 3 passed。
🎭 Mock 的价值:造出「等不到」的错误
真实接口出的错分五类:连不上、超时、4xx、5xx、200 但内容不对。 requests 的超时与异常处理见官方文档,状态码语义见 MDN 参考。
前四类你运气好能碰到。最后一类你碰不到——因为它只在特定数据下发生。
哪个订单会「状态码 200 但金额算错」?猜不到。只能自己造。
我们的假服务提供这些路径:
| 路径 | 方法 | 模拟什么 |
|---|---|---|
/orders/9999 | GET | 资源不存在 |
/broken-json | GET | 200 但内容不是合法 JSON |
/wrong-shape | GET | 200 但结构不对 |
/slow?delay=2 | GET | 慢到触发超时 |
/boom | POST | 500 服务端崩了 |
逐个试:
--- 200,但内容不是合法 JSON ---
状态码:200
.text :{"orders": [
.json():抛异常了 -> JSONDecodeError
--- 200,但结构不对 ---
状态码:200
.json():{'orders': '这不是列表,是一段文字'}
注意最后这条:状态码 200,一切看起来正常。 但 orders 是文字不是列表,下游一 for 就炸。
客户端在这两种情况下的行为完全不同:
- 坏 JSON:
.text给你字符串,.json()抛JSONDecodeError - 结构错:
.json()正常返回一个 dict,但字段类型不对
所以测试断言不能只查状态码,得查你实际要用的那个字段。 这正是 Day28 讲的四层断言。
🧾 造数据:整齐的数据是有问题的
一开始我以为造数据很简单——编号连着、金额两位小数,五笔就够:
def 造一批订单(数量=5, 起始编号=2001):
订单们 = []
金额 = 99.80
for i in range(数量):
订单们.append(造订单(str(起始编号 + i), f"{金额:.2f}"))
金额 = 金额 + 100
return 订单们
这种数据只能测出「正常时对不对」,测不出「不正常时崩不崩」。
所以数据要分两批。边界值和坏值:
def 造边界金额():
"""金额的边界和坏值。"""
return [
("0.00", "零元单,边界"),
("0.01", "最小金额,边界"),
("99999999.99", "最大金额,边界"),
("199.8", "少一位小数,坏值"),
("199.800", "多一位小数,坏值"),
("199.80 ", "尾部有空格,坏值"),
("-19.80", "负数,坏值"),
("abc", "不是数字,坏值"),
("", "空字符串,坏值"),
]
挨个发给接口:
本该拒绝、却被放行的坏值,一共 6 个:
'199.8' 少一位小数,坏值 状态码 201
'199.800' 多一位小数,坏值 状态码 201
'199.80 ' 尾部有空格,坏值 状态码 201
'-19.80' 负数,坏值 状态码 201
'abc' 不是数字,坏值 状态码 201
'' 空字符串,坏值 状态码 201
全放行了。 服务只检查 isinstance(amount, str),所以连 "abc" 和空字符串都收。
这是接口的业务漏洞,Mock 让它十分钟就能发现。
边界值有两层
"0.00" 也被收下了,但这个不算错——零元单在有些业务里合法(比如全额优惠)。
| 层 | 例子 | 谁来答 |
|---|---|---|
| 技术上合不合法 | 负数、空字符串 | 服务端该拦 |
| 业务上允不允许 | 零元单 | 得问产品 |
第二个问题代码回答不了。 测试能测出「服务端没收住负数」,但「零元单该不该允许」要去问人。
🤖 AI 造数据:问题都在「整批」层面
让 AI 造 5 笔订单数据,它给的:
AI_原始数据 = [
{"order_id": "1001", "status": "PAID", "amount": "199.80"},
{"order_id": "1002", "status": "PAID", "amount": "199.80"},
{"order_id": "1003", "status": "PAID", "amount": "199.80"},
{"order_id": "1004", "status": "PAID", "amount": "199.80"},
{"order_id": "1005", "status": "PAID", "amount": "199.80"},
]
单条看每一条都合规:字符串、两位小数、状态合法、编号不重复。
所以我的第一版审查函数审出 0 个问题。
但它明明有问题。问题不在单条上,在整批上。
单条审查
def 审一条(记录, 已知编号集合):
问题 = []
金额 = 记录.get("amount")
if not isinstance(金额, str):
问题.append(f"amount 不是字符串,是 {type(金额).__name__}")
else:
try:
数值 = float(金额)
except ValueError:
问题.append(f"amount {金额!r} 不是数字")
else:
if 数值 < 0:
问题.append(f"amount {金额!r} 是负数")
if f"{数值:.2f}" != 金额:
问题.append(f"amount {金额!r} 不是两位小数")
if 记录.get("status") not in ("CREATED", "PAID", "CANCELLED"):
问题.append(f"status {记录.get('status')!r} 不是接口认的状态")
if 记录.get("order_id") in 已知编号集合:
问题.append(f"order_id {记录.get('order_id')!r} 重复了")
return 问题
整批审查
if 记录们:
状态们 = {r.get("status") for r in 记录们}
if len(状态们) == 1:
全部问题.append(("整批", f"状态全是 {状态们.pop()!r},等于只测了一种情况"))
金额们 = [r.get("amount") for r in 记录们]
if len(set(金额们)) == 1:
全部问题.append(("整批", f"金额全是 {金额们[0]!r},等于没造数据"))
撞上的 = 已知编号 & 固定快照编号
if 撞上的:
全部问题.append(("整批", f"编号 {sorted(撞上的)} 和固定快照撞了,会覆盖掉它"))
补上之后:
订单 整批:5 笔的状态全是 'PAID',等于只测了一种情况
订单 整批:5 笔的金额全是 '199.80',等于没造数据
订单 整批:编号 ['1001', '1002', '1003', '1004'] 和固定快照撞了,会覆盖掉它
一共 3 个问题。
第三条最值得警惕
编号 1001—1005 —— 那是 Day19 那份固定快照的编号。
如果这批数据被当成「新数据」传进去,你会覆盖掉固定快照。 而 AI 不知道你项目里已经有哪些编号,它只会从 1001 开始编。
这不是 AI 笨,是它缺上下文。你项目里有哪些编号、哪些数据不能动,提示词里没说,它无从得知。
📝 修法:提示词里多说三句
请造 5 笔订单测试数据,order_id 从 9001 开始连号(不要用 1001—1005,
那是我固定快照的编号),status 要覆盖 CREATED、PAID、CANCELLED 三种,
amount 全部是两位小数的非负数字符串,每个金额都不一样。
重造后审出 0 个问题。
但光靠这些还是不够
这 5 笔全是正常数据。边界和坏值这部分 AI 不会主动给,因为提示词里没写。
更省事的做法:每次让 AI 造完,用固定脚本审一遍。 审的脚本是固定的,AI 换几批数据它都适用。
🧪 把「发现漏洞」变成一条测试
上面那 6 个坏值被放行,是一次性的发现。要让它不再溜过去,得写成测试:
坏值 = ["199.8", "199.800", "199.80 ", "-19.80", "abc", ""]
def test_坏金额应该被拒绝(本地服务):
放行了 = []
for 金额 in 坏值:
响应 = requests.post(f"{本地服务}/orders", json={"amount": 金额}, timeout=5)
if 响应.status_code == 201:
放行了.append(金额)
assert 放行了 == [], f"这些非法金额被收下了:{放行了}"
这条测试现在是红的——因为服务端确实没收住。这正是它该有的样子。
等服务端加上金额格式校验,它就变绿了。
一个好的漏洞发现,最终应该变成一条一直亮着的测试。 否则半年后有人重构把这个校验删了,没人知道。
📊 Mock 该造哪些错误
不是所有错误都值得 Mock。按「等得到的」和「等不到的」分:
| 错误 | 等得到吗 | 要不要 Mock |
|---|---|---|
| 404 资源不存在 | 等得到(造个不存在的编号) | 不用 Mock |
| 400 参数错 | 等得到(传个错值) | 不用 Mock |
| 超时 | 偶尔等得到 | 建议 Mock |
| 500 服务端崩 | 难得 | 建议 Mock |
| 200 但内容不对 | 等不到 | 必须 Mock |
| 响应特别慢 | 等不到 | 必须 Mock |
「必须 Mock」那两行的共同点:真实环境里几乎复现不了。 一次 500 可能一年出一次,你不可能每次回归都等到它。
📋 一份数据审查清单
写完审查脚本之后,可以固化成这样:
单条规则
□ 必填字段都在吗?
□ 类型对吗?(金额是字符串还是数字,别混)
□ 格式对吗?(两位小数、没有前后空格)
□ 值合法吗?(金额非负、编号不重复)
□ 状态是接口认的吗?(不在枚举里就是非法)
整批规则
□ 状态覆盖了几种?(全一样 = 只测了一种情况)
□ 金额是不是全都一样?(全一样 = 没造数据)
□ 编号会不会撞到已有数据?(AI 最爱从 1001 开始编)
边界与坏值
□ 0、最小值、最大值、刚好超一点
□ 少一位小数、多一位小数、尾部空格
□ 负数、空字符串、非数字、空列表
□ 业务上不允许但技术上合法的(零元单)
最后一类最容易漏。 「技术上合法但业务上不允许」,代码查不出来,只能靠人知道。
🧮 一个能自我验证的小设计
审查脚本本身也可以被审查。
def test_审查脚本能抓到坏数据():
坏数据 = [{"order_id": "1001", "status": "PAID", "amount": "-19.80"}]
assert 审一批(坏数据), "审查脚本对负数金额竟然没报错"
这条测试保证你的审查脚本不是摆设。
和 Day28 那个「坏响应测试」是同一个思路——检查逻辑本身也要被检查。 这条线索贯穿了 Day28 到 Day30。
🔬 一个延伸:Mock 的参数化测试
Mock 最适合用参数化写——每个错误一条,失败时一眼看出是哪类:
错误场景 = [
("资源不存在", "/orders/9999", "GET", 404),
("内容不是 JSON", "/broken-json", "GET", "JSONDecodeError"),
("结构不对", "/wrong-shape", "GET", None),
("服务端崩了", "/boom", "POST", 500),
]
@pytest.mark.parametrize("名字,路径,方法,预期", 错误场景, ids=[x[0] for x in 错误场景])
def test_每类错误客户端都能正确识别(名字, 路径, 方法, 预期, 本地服务):
if 方法 == "GET":
响应 = requests.get(f"{本地服务}{路径}", timeout=5)
else:
响应 = requests.post(f"{本地服务}{路径}", json={}, timeout=5)
if 预期 == "JSONDecodeError":
with pytest.raises(Exception):
响应.json()
elif 预期 is None:
assert 响应.status_code == 200 # 状态码正常,但内容不对
assert not isinstance(响应.json()["orders"], list) # 这才是关键断言
else:
assert 响应.status_code == 预期
最后那个 assert not isinstance(...) 才是重点。 状态码 200 时,「内容类型不对」这条断言才真正起作用。
这也是 Day28 那个「四层断言」的翻版——响应码正常的时候,你唯一能查的就是内容本身。
📝 三个练习
答案在 04_answer.py,9 条断言,pytest 3 passed。
练习一:审 AI 那批数据,答案是 3 个问题,而且全是「整批」问题。
练习二:审合格批次,答案 0 个问题。
练习三:把 6 个坏值挨个发给接口,答案 全部 201 被放行。测试这边只能如实记录,修法是服务端加金额格式校验。
🧾 一份可复用的审查清单
写完之后可以固化成这样,每次造完数据过一遍:
□ 单条:字段在不在?类型对吗?格式对吗?值合法吗?编号重复吗?
□ 整批:状态覆盖了几种?金额是不是全都一样?
□ 整批:编号会不会撞到已有数据?
□ 边界:0、最大值、刚好超一点,这些试了吗?
□ 坏值:少一位小数、负数、空值、非数字,试了吗?
🧾 一条龙:Mock 出来的问题,怎么变成回归
这是 Mock 最有价值的闭环,前面几节是零件,这里是组装:
1. 用 Mock 造出坏响应 → 复现问题
2. 写一条断言 → 描述「什么算对」
3. 断言对着 Mock 跑 → 红了,确认它抓得住
4. 修服务端 → 同样的输入,Mock 返回 200
5. 断言再跑 → 绿了,确认修好了
6. 把这条测试挂到 CI → 以后每次改动都会验一遍
第 3 步最容易被跳过。 直接从第 2 步跳到第 4 步,你就不知道那条断言到底抓不抓得住错——这正是 Day28 讲的问题。
第 6 步决定了这个闭环是不是临时的。 只修一次不挂 CI,三个月后有人重构把校验删了,问题原样回来,而且没人知道它曾经被修过。
🔍 一个容易忽略的 Mock 陷阱
Mock 造的接口和真接口长得一样,但它们不是同一个东西。
真实接口有个特性叫幂等:同样的请求发两次,第二次要么被拒绝(409),要么返回「没发生新的变更」(changed=False)。这是 Day26 讲的那个细节。
如果你的 Mock 每次都返回 changed=True,那它测出来的东西是假的。
真实接口还有:超时之后服务端可能已经把事做完了(网络断了,不代表服务端没执行)。Mock 复现不了这个,因为它没有「执行到一半」的概念。
所以 Mock 要照着真接口的行为造,不是照着你希望它有的行为造。
最省事的办法:先拿真接口跑一遍,把它的真实输出抄进 Mock。 抄完之后 Mock 才可信。
📌 一句话总结今天的两条线
Mock 那条线:造出你等不到的错误,让「200 但内容不对」这种问题从碰运气变成必测。
数据那条线:让 AI 造数据省下抄参数的力气,但审查的判断必须自己留着——单条审不够,还得整批审。
两条线合起来是一句话:你得自己造出错的条件,也自己判断什么算对。
🚧 今天没做的
- 没有做 Mock 服务框架。 现在是自己写的教学服务,真实项目里会用现成的 Mock 工具或测试替身。
- 没有测并发下的数据一致性。 多请求同时创建会不会撞编号,属于 Day33 的链路编排。
- 没有覆盖性能。 造一万条数据要多久,是 Day30 的内容。
下一步是 Day30《接口性能 + AI 压测分析》,我们关心「快不快」,但会先说清楚一个最容易搞混的事:并发用户数不等于请求速率。
更多推荐



所有评论(0)