接口链路里最容易出错的三个地方,出问题时通常都不报错,只是结果不对:值从哪来、失败了怎么办、什么算对。
这一篇把三者都变成配置里的显式约定。
配套脚本在 examples/lab/day33/,四个文件都能直接跑。代码在 Windows、Python 3.11 下实跑,pytest 3 passed。
🔗 一、参数关联:值到底从哪来
每一步要用的值只有三个来源:写死的、上一步产生的、更早某一步产生的。
写死的问题 Day31 讲过——换环境就没了。
用配置写出来:
链路 = [
{"步骤": "创建订单", "方法": "POST", "路径": "/orders",
"请求体": {"user_id": "u1", "amount": "199.80"},
"期望状态码": 201,
"产生": {"order_id": "响应.order_id"}},
{"步骤": "支付订单", "方法": "POST", "路径": "/orders/{order_id}/pay",
"请求体": {"status": "SUCCESS"}, "期望状态码": 200, "产生": {}},
{"步骤": "取消订单", "方法": "POST", "路径": "/orders/{order_id}/cancel",
"请求体": {}, "期望状态码": 409, "产生": {}},
{"步骤": "查订单", "方法": "GET", "路径": "/orders/{order_id}",
"请求体": None, "期望状态码": 200, "产生": {}},
]
「产生」这一栏就是「上一步喂给下一步」的全部含义。requests 见官方快速上手,pytest.raises 见官方文档,JSON 读写见 Python 官方文档。
执行器
def 取字段(响应体, 路径串):
"""从响应里按 '响应.order_id' 这种写法取字段。"""
if 路径串.startswith("响应."):
路径串 = 路径串[len("响应."):]
当前 = 响应体
for 一段 in 路径串.split("."):
if isinstance(当前, dict) and 一段 in 当前:
当前 = 当前[一段]
else:
raise KeyError(f"响应里没有 {路径串!r},现有:{当前}")
return 当前
def 跑链路(base, 步骤们, 变量=None):
变量 = dict(变量 or {})
每步结果 = []
for 步骤 in 步骤们:
路径 = 步骤["路径"]
for 名字, 值 in 变量.items():
路径 = 路径.replace("{" + 名字 + "}", str(值))
if "{" in 路径:
raise ValueError(f"步骤「{步骤['步骤']}」的地址还有没替换的变量:{路径}")
if 步骤["方法"] == "GET":
响应 = requests.get(f"{base}{路径}", timeout=5)
else:
响应 = requests.post(f"{base}{路径}", json=步骤["请求体"], timeout=5)
每步结果.append({"步骤": 步骤["步骤"], "状态码": 响应.status_code,
"响应": 响应.json()})
for 变量名, 来源 in 步骤.get("产生", {}).items():
变量[变量名] = 取字段(响应.json(), 来源)
return 每步结果, 变量
取字段 里的 raise KeyError 不能省。 字段名写错时它要说清楚「响应里没有这个字段,现有的是什么」,而不是抛一个 KeyError: 'order_id' 让人自己猜。
那个检查的价值
假设有人改了链路,把第一步的「产生」删了:
步骤「支付订单」的地址还有没替换的变量:/orders/{order_id}/pay
这一步就炸了,而且说得很清楚。
不加检查的话,它会发一个地址里带着 {order_id} 的请求,服务端返回 404,然后你得先想到「哦是变量没替换」。
这个检查把「猜」变成「看」。
🔀 二、失败分支:停还是跳过
用一条会失败的链路:创建 → 支付 → 取消(已支付,必然 409) → 查订单 → 查支付记录。
策略一:失败就停
def 策略_遇到失败就停(base, 步骤们, 变量=None):
变量 = dict(变量 or {})
结果 = []
for 步骤 in 步骤们:
# … 发请求、记结果 …
if 响应.status_code >= 400:
结果[-1]["处理"] = "失败,停在这里"
return 结果, 变量, "停止" # 直接结束整条链路
return 结果, 变量, "全部跑完"
实际跑了 3 步,后两步没跑。
策略二:失败就跳过
if 响应.status_code >= 400:
结果[-1]["处理"] = "失败,后面跳过"
跳过中 = True
五步全记下来了,但后两步没真跑。
怎么选
关键问题是:后面的步骤依赖前面吗?
| 查订单 | 依赖,取消失败它就没意义 | 停 |
| 查支付记录 | 不依赖,取消失败它还能解释原因 | 继续跑 |
所以这两步其实该分开处理。 「失败就停」是全局策略,它太粗。
所以正确做法是按步写
{
"步骤": "查订单",
"方法": "GET",
"路径": "/orders/{order_id}",
"请求体": null,
"期望状态码": 200,
"依赖": ["创建订单"],
"依赖失败时": "跳过"
}
「依赖失败时」这一栏是今天的重点:写「跳过」还是「停止」,取决于这一步还值不值得跑。
依赖写清楚之后,这个判断就从人的脑子里搬到了配置里。
✅ 三、每步写清期望状态码
上面那条链路里第三步返回 409。这是失败吗?
不是。「取消已支付的订单会被拒绝」正是这一步要验的东西。
为什么不能偷懒用「>= 400 就当失败」
因为那样的话,「取消已支付订单应该被拒绝」这条用例永远跑不过。
跑不过,大家就会习惯性地忽略 409——然后真正的 409 也被忽略了。
这是接口测试里最隐蔽的一种麻木:一个永远红的断言,等于没有断言。
改成「和这一步自己写的期望比」:
期望 = 步骤.get("期望状态码")
实际 = 响应.status_code
符合 = (期望 is None) or (实际 == 期望)
if 符合:
for 变量名, 来源 in 步骤.get("产生", {}).items():
变量[变量名] = 响应.json()[来源.replace("响应.", "")]
else:
结果[-1]["处理"] = "不符合期望"
if 失败就停:
停 = True
实跑:
创建订单 实际 201 期望 201 符合
支付订单 实际 200 期望 200 符合
取消已支付的订单 实际 409 期望 409 符合
查订单 实际 200 期望 200 符合
第三步实际 409、期望 409,它是绿的。
期望写错了会怎样
把第三步期望错写成 200:
创建订单 实际 201 期望 201 符合
支付订单 实际 200 期望 200 符合
取消已支付的订单 实际 409 期望 200 不符合 ← 抓到了
查订单 实际 跳过 期望 – 不符合 ← 抓到了
接口返回 409 是正确的,但用例期望 200,所以用例红了。红的是用例,不是接口。
一个实现细节
跳过的那一步在结果里没有「期望」字段(它根本没跑)。所以格式化输出时要用 .get("期望", "-"),别直接下标——否则跳过的步骤会让整个报告脚本崩掉。
🔁 依赖关系为什么必须写进配置
因为「谁依赖谁」是脑子里的知识,而脑子里的知识不会被 review。
{
"步骤": "查订单",
"方法": "GET",
"路径": "/orders/{order_id}",
"请求体": null,
"期望状态码": 200,
"依赖": ["创建订单"],
"依赖失败时": "跳过"
}
有了这两行:
- 别人插入新步骤时,能看出这一步受什么影响
- 执行器知道哪一步挂了该跳过哪一步
- 「查支付记录」这种不依赖任何东西的步骤,写 依赖: [],它就永远跑
一个真实场景:链路跑到一半断了
线上排查常见这个需求:「跑到第 3 步失败了,看看当时的数据是什么样」。
如果链路是「失败就停」,你只有前 3 步的信息。 但第 4、5 步的数据可能才是关键——第 4 步是「查支付记录」,它能解释第 3 步为什么失败。
所以「停」和「跳过」不是二选一,要按步写。
🧩 一个实现上的细节:变量表的传递
def 跑链路(base, 步骤们, 变量=None):
变量 = dict(变量 or {})
…
for 变量名, 来源 in 步骤.get("产生", {}).items():
变量[变量名] = 取字段(响应.json(), 来源)
return 每步结果, 变量
变量表是链路的「公共黑板」,每一步都往里写、也从里面读。
好处是简单,坏处是名字会撞。如果两个不同的步骤都产生 order_id,后一个会覆盖前一个。
所以变量名最好带上来源:
{
"产生": {"创建订单的order_id": "响应.order_id"}
}
代价是路径变长了(/orders/{创建订单的order_id})。两种做法我一般选后者——变量撞名是那种「跑一次没事、跑十次偶尔错一次」的问题,比路径长难查得多。
🧪 链路本身也可以被测
def test_链路的每一步都符合期望(本地服务):
结果, 变量 = 跑链路(本地服务, 链路)
assert all(r.get("符合") for r in 结果)
assert 变量["order_id"] == "2001"
这和测单个接口不一样。 单个接口的测试只看那一次响应;链路测试看的是「整条流程走下来,每一步的前提都成立」。
一个常见 bug 是:第一步造的数据被第二步改坏了,但每一步单独看都绿。 链路测试能抓到。
📋 一份能直接抄的链路模板
{
"步骤": "查订单",
"方法": "GET",
"路径": "/orders/{order_id}",
"请求体": null,
"期望状态码": 200,
"依赖": ["创建订单"],
"依赖失败时": "跳过"
}
四个字段一个都不能少:
- 期望状态码 —— 不写就退回「>= 400 就当失败」,那条永远红的用例就回来了
- 依赖 —— 依赖哪一步产出的变量
- 依赖失败时 —— 跳过还是停止
- 方法 / 路径 / 请求体 —— 请求本身
「依赖失败时」写「跳过」还是「停止」,取决于这一步还值不值得跑。
撞名具体是怎么发生的
变量表就是一个普通字典,键是变量名,值是内容:
变量 = {}
变量["order_id"] = "2001" # 第一步写入
变量["order_id"] = "2002" # 第二步又写了一个,直接把上一个顶掉
print(变量) # {'order_id': '2002'}
没有任何警告。 第一步产出的 2001 就这么没了,而真正需要它的第三步会拿到 2002——然后拿着一个不存在的订单号去查,接口回 404,你却不知道为什么。
所以这条规矩不是「好看」,是防止一个静默的覆盖。
🔁 一个容易被忽略的规矩
变量名要能看出它从哪来。
变量表是链路的公共黑板,两个步骤都产生 order_id 就会撞名。而撞名这种 bug 不是必现的——取决于步骤跑到哪、执行顺序、当时表里有什么。
所以变量名带上来源:
{
"产生": {"创建订单的order_id": "响应.order_id"}
}
路径变长了,但变量撞名是那种「跑一次没事、跑十次偶尔错一次」的问题,比路径长难查得多。
🔍 一个能立刻用起来的动作
拿你手上任意一条链路,把这三处补上:
改完你会发现,链路里那些「我记得它应该失败」的地方,终于写下来了。
而这些原本都藏在某个人脑子里,或者藏在某个人的记性里。显式约定才能被 review,脑子里的不能。
📝 三个练习
答案在 04_answer.py,9 条断言,pytest 3 passed。
练习一:变量传下去。变量 == {"order_id": "2001"},且第 2、3、4 步用的都是同一个号。
练习二:期望 409 是绿的。第三步实际 409、期望 409、判定符合,且全部步骤都符合。
练习三:期望写错时红的是用例。接口仍返回 409(对的),但用例期望 200,判不符合。
📌 今天带走的判断规则
三样都写成配置里的显式约定:值从哪来、失败了怎么办、什么算对。
理由不是「配置比代码好看」,而是这三样出问题时通常不报错。隐含约定只在写代码的人脑子里,显式约定才能被 review。
🚧 今天没做的
- 没有做真正的依赖图排序。 今天靠配置里的顺序表达依赖,没做拓扑排序
- 没有做重试。 失败后重试几次、退避多久,是另一个决定
- 没有做并发链路。 步骤之间并发执行,需要处理变量冲突
- 没有做链路的断言。 今天断言的是每一步的响应,整条链路的结论(业务上成功了吗)没单独验
下一步是 Day34《接口任务执行:状态流转、取消与结果保存》,我们跑那种「发出去就不用管了」的任务。
网硕互联帮助中心






评论前必须登录!
注册