1. 从一个真实困境说起:接口之间的依赖关系
做接口测试的人几乎都遇到过这种场景:你手里有两个接口,A接口负责查询或提交业务数据,B接口负责生成某种凭证、令牌或者前置ID。A接口的请求参数里,有一个字段必须填入B接口返回的结果,否则A接口直接报参数校验失败。这时候如果你还在手动操作——先调B、复制返回值、粘贴到A的参数里、再点发送——那效率低得让人抓狂,而且一旦B的返回值有时效性,你复制粘贴的功夫它可能就过期了。
这个问题的本质是接口依赖,或者叫接口串联。在Postman里解决它,核心思路只有一句话:把B接口的返回值提取出来,存进一个变量,然后在A接口的请求里引用这个变量。听起来简单,但真正落地时会牵扯出一连串细节问题:返回值是JSON还是XML?字段嵌套多深?提取语法怎么写?变量存在哪个作用域?B接口什么时候执行?A接口怎么触发B接口?这些问题一个没处理好,整个链路就跑不通。
我见过太多人卡在第一步——不知道Postman有"Pre-request Script"这个功能,或者知道但不会写提取脚本。也有人用环境变量手动传值,结果团队协作时变量不同步,别人拉下代码跑不起来。还有人把B接口的返回值硬编码进A接口,B一改逻辑整个测试就崩了。
这篇文章就是把这些坑一个个填平。不管你是刚接触Postman的新手,还是用了几年但一直靠手动传参的老手,下面这套方法都能直接抄作业。我会从最基础的变量作用域讲起,到提取脚本的写法,再到用Collection Runner和Newman做自动化串联,最后分享几个我踩过的真实坑和排查思路。全程不依赖任何外部工具,Postman原生功能就能搞定。
2. 先搞懂Postman的变量体系,否则后面全是坑
2.1 五种变量作用域的真实优先级
Postman的变量不是随便存的,它有一套严格的作用域层级。很多人提取脚本写对了,但A接口就是取不到值,十有八九是变量存错了地方。Postman的变量从大到小分为五种:
| 作用域类型 | 生效范围 | 典型用途 |
|---|---|---|
| Global | 整个Postman应用内所有请求 | 全局配置,如基础域名 |
| Collection | 当前集合内所有请求 | 集合级公共参数 |
| Environment | 当前选中的环境内 | 不同环境的差异化配置 |
| Data | 仅在Collection Runner运行时有效 | 数据驱动测试的CSV/JSON数据 |
| Local | 仅在当前请求或脚本运行期间有效 | 临时变量,脚本内传递 |
优先级顺序是:Local > Data > Environment > Collection > Global。也就是说,如果同名变量在多个作用域都存在,Postman会取优先级最高的那个。这个规则解释了一个常见困惑:为什么我在环境变量里改了值,请求里用的还是旧值?因为集合变量里有个同名的,它优先级更高,把环境变量盖住了。
我的建议很明确:接口依赖产生的动态值,优先存Environment变量。原因是环境变量可以在团队间通过导出环境文件共享,而且切换环境时不会互相污染。如果你只是临时调试,用Local变量也行,但Local变量在请求结束后就销毁了,不适合跨请求传递。
2.2 变量的读写语法:pm对象与旧版语法的区别
Postman的脚本API经历过一次大改版。老版本用postman.setEnvironmentVariable("key", "value")这种写法,新版本统一用pm对象:
// 写入环境变量 pm.environment.set("token", "abc123"); // 读取环境变量 const token = pm.environment.get("token"); // 写入集合变量 pm.collectionVariables.set("userId", "1001"); // 写入全局变量 pm.globals.set("baseUrl", "https://api.example.com"); // 写入局部变量(仅在当前请求生命周期有效) pm.variables.set("tempValue", "xyz");pm.variables.set这个要特别说一下,它写入的是一个"临时变量",优先级最高,但只在当前请求的脚本执行期间有效。如果你在Pre-request Script里set,然后在Tests里get,是取不到的。所以跨请求传递千万别用它。
还有一个容易忽略的点:pm.environment.get在变量不存在时返回undefined,不会报错。这意味着如果你的提取脚本因为某种原因没执行成功,A接口拿到的就是undefined,然后请求发出去参数是空的,接口返回一个莫名其妙的错误。排查时你根本想不到是变量没取到。所以我在提取脚本里习惯加一层判断:
const token = pm.response.json().data.token; if (token) { pm.environment.set("authToken", token); console.log("Token提取成功:" + token); } else { console.error("Token提取失败,响应结构可能变了"); }这个console.log在Postman的Console里能看到,调试时非常有用。很多人不知道Postman有Console(快捷键Ctrl+Alt+C或菜单View→Show Postman Console),所有脚本的日志都输出在那里,是排查脚本问题的第一现场。
2.3 环境变量的导出与团队协作
环境变量存在本地,默认不会同步给团队其他人。如果你把集合分享出去,别人拉下来发现所有依赖变量都是空的,整个链路跑不起来。解决办法是导出环境文件(JSON格式),连同集合一起发给团队,别人导入后就能直接用。
导出的环境文件里,变量的值如果是敏感信息(比如真实令牌),建议在导出前清空或替换成占位符。Postman支持"敏感变量"标记,把变量类型设为secret后,值在界面上会显示为星号,导出时也会被隐藏。这个功能在团队协作时很实用,避免令牌泄露。
3. 提取B接口返回值的三种实战写法
3.1 JSON响应:用pm.response.json()直接取字段
绝大多数现代接口返回的都是JSON。假设B接口的响应体长这样:
{ "code": 200, "message": "success", "data": { "userId": 10086, "token": "eyJhbGciOiJIUzI1NiIs...", "expireAt": "2025-01-01T00:00:00Z" } }我要提取data.token,在B接口的Tests标签页里写:
// 先解析响应体为JSON对象 const res = pm.response.json(); // 逐层取值,注意每一层都要判断是否存在 if (res && res.data && res.data.token) { pm.environment.set("authToken", res.data.token); console.log("提取到token:" + res.data.token); } else { console.error("响应结构不符合预期,实际响应:" + JSON.stringify(res)); }这里有个细节:pm.response.json()在响应体不是合法JSON时会抛异常,导致整个脚本中断。如果你不确定B接口一定返回JSON,可以先判断Content-Type:
if (pm.response.headers.get("Content-Type").includes("application/json")) { const res = pm.response.json(); // ...提取逻辑 } else { console.warn("响应不是JSON格式,跳过提取"); }嵌套层级深的时候,一层层写res.data.list[0].id很容易出错。我习惯用可选链操作符(Optional Chaining)简化:
const token = pm.response.json()?.data?.token;一个问号加点的写法,任何一层是undefined或null都会直接返回undefined,不会报错。Postman内置的脚本引擎支持ES2020语法,可选链是可以用的。但要注意,如果你的Postman版本比较老(v9以下),可能不支持,那就老老实实写判断。
3.2 数组响应:定位到具体元素再提取
B接口返回的是一个列表,你需要的值在某个特定元素里。比如查询用户列表,返回:
{ "code": 200, "data": { "list": [ {"id": 1, "name": "张三", "role": "admin"}, {"id": 2, "name": "李四", "role": "user"} ] } }我要提取role为admin的那个用户的id,用find方法:
const list = pm.response.json()?.data?.list || []; const adminUser = list.find(item => item.role === "admin"); if (adminUser) { pm.environment.set("adminId", adminUser.id); console.log("找到管理员ID:" + adminUser.id); } else { console.error("列表中不存在role为admin的用户"); }如果列表为空或者没有匹配项,find返回undefined,后面的adminUser.id就会报错。所以一定要先判断。这里用|| []兜底,防止list本身是undefined时find报错。
还有一种情况是取数组的第一个或最后一个元素:
const firstId = pm.response.json()?.data?.list?.[0]?.id; const lastId = pm.response.json()?.data?.list?.slice(-1)?.[0]?.id;slice(-1)取最后一个元素组成的新数组,再取[0]。这个写法比list[list.length - 1]更安全,因为空数组时slice(-1)返回空数组,再取[0]得到undefined,不会报错。
3.3 非JSON响应:正则表达式兜底提取
有些老接口返回的是XML、HTML甚至纯文本。这时候pm.response.json()直接歇菜。用正则从文本里抠出需要的值:
const text = pm.response.text(); // 假设响应里有 <token>abc123</token> 这样的结构 const match = text.match(/<token>(.*?)<\/token>/); if (match && match[1]) { pm.environment.set("authToken", match[1]); console.log("正则提取到token:" + match[1]); } else { console.error("正则未匹配到token,原始响应:" + text); }正则里的.*?是非贪婪匹配,防止匹配到多个标签之间的内容。如果响应里有多个同名标签,match只返回第一个匹配结果。要取所有匹配,用matchAll或者加g标志配合循环。
XML响应其实有更好的处理方式——Postman内置了xml2Json库,可以在Tests里直接调用:
const xmlBody = pm.response.text(); const jsonObj = xml2Json(xmlBody); const token = jsonObj?.response?.token;xml2Json是Postman沙箱环境预置的库,不需要额外引入。但它的转换规则有时和预期不一致,属性会带@前缀,文本节点可能变成#text。用之前最好先console.log(JSON.stringify(jsonObj))看一眼转换后的结构。
4. 让A接口自动拿到B接口的值:四种触发方式
4.1 Pre-request Script:在A请求发送前主动调用B
这是最"自动化"的方式。A接口的Pre-request Script里直接发一个请求去调B,拿到返回值存变量,然后A请求继续发送。整个过程用户只需要点一次Send。
// A接口的Pre-request Script const bRequest = { url: pm.environment.get("baseUrl") + "/api/getToken", method: "POST", header: { "Content-Type": "application/json" }, body: { mode: "raw", raw: JSON.stringify({ appId: pm.environment.get("appId"), appSecret: pm.environment.get("appSecret") }) } }; pm.sendRequest(bRequest, function (err, response) { if (err) { console.error("调用B接口失败:" + err); return; } const res = response.json(); if (res && res.data && res.data.token) { pm.environment.set("authToken", res.data.token); console.log("Pre-request中获取token成功"); } else { console.error("B接口响应异常:" + JSON.stringify(res)); } });这里有个关键点:pm.sendRequest是异步的。也就是说,脚本不会等它执行完再发A请求。如果你的A请求在Header里引用了{{authToken}},而pm.sendRequest的回调还没执行完,A请求发出去时变量还是旧值或者空值。
这个问题困扰了很多人。Postman的Pre-request Script执行机制是:同步代码执行完就发请求,异步回调不等待。所以上面的写法在大多数情况下会失败——A请求发出时token还没写入。
解决办法有两个。第一个是用async/await配合pm.sendRequest的Promise版本(Postman v10+支持):
// 使用async函数包裹 (async () => { const bRequest = { url: pm.environment.get("baseUrl") + "/api/getToken", method: "POST", header: {"Content-Type": "application/json"}, body: { mode: "raw", raw: JSON.stringify({appId: "xxx", appSecret: "yyy"}) } }; try { const response = await pm.sendRequest(bRequest); const res = response.json(); if (res?.data?.token) { pm.environment.set("authToken", res.data.token); console.log("token已写入环境变量"); } } catch (err) { console.error("请求B失败:" + err); } })();但即使这样,Postman也不保证会等待这个async函数执行完。实测下来,在Pre-request Script里用async/await有时能生效,有时不行,取决于Postman版本和请求的复杂程度。这不是一个稳定的方案。
第二个办法更可靠:把B接口单独作为一个请求,放在A接口前面,用Collection Runner顺序执行。B接口的Tests里提取变量,A接口直接引用。这样B执行完才轮到A,不存在异步竞争问题。这也是我推荐的生产级做法。
4.2 Tests脚本提取 + 手动顺序执行
这是最基础也最可控的方式。B接口的Tests里写好提取脚本,先手动Send B,看到Console里打印"提取成功",再Send A。A的请求参数里用{{authToken}}引用。
这种方式适合调试阶段,你能清楚看到每一步的变量变化。缺点是每次都要手动点两次,接口多了很繁琐。而且如果B的token有时效性(比如5分钟过期),你调完B去干别的,回来再调A就过期了。
4.3 Collection Runner:批量顺序执行,自动传递
Collection Runner是Postman内置的批量执行工具。你把B和A按顺序放进一个集合,Runner会依次执行,B的Tests里写入的变量,A执行时就能读到。
操作步骤:
- 新建一个Collection,把B接口和A接口都加进去,拖动调整顺序,B在前A在后。
- 点击Collection右侧的"Run"按钮,打开Runner界面。
- 选择要执行的请求(默认全选),设置迭代次数(Iterations)为1。
- 点击"Run"开始执行。
Runner执行时,每个请求的Tests脚本都会运行,变量在请求之间自然传递。你可以在Runner的结果面板里看到每个请求的通过/失败状态,以及Console里的日志。
这里有个坑:Runner默认会按照集合里的顺序执行,但如果你在请求的Tests里用了postman.setNextRequest,顺序会被打乱。检查一下有没有遗留的跳转脚本。
另一个坑是环境选择。Runner界面右上角要选对Environment,否则变量写到了A环境,请求读的是B环境,取不到值。我习惯在Runner启动前先确认环境下拉框选的是目标环境。
4.4 Newman命令行:CI/CD流水线里的接口串联
如果你要把这套接口依赖放到持续集成里跑,Newman是Postman官方的命令行工具。先导出Collection和环境文件,然后:
# 安装Newman npm install -g newman # 执行集合,指定环境文件 newman run my-collection.json -e my-environment.json --reporters cli,json # 带HTML报告 newman run my-collection.json -e my-environment.json -r htmlextra --reporter-htmlextra-export report.htmlNewman执行时,变量传递逻辑和Runner完全一致。B接口的Tests写入环境变量,A接口读取。但要注意:Newman运行时环境变量是内存中的,不会写回你的环境文件。每次执行都是全新的环境副本,所以不用担心上次的残留值污染。
如果B接口的返回值需要持久化到文件供后续步骤使用,可以在Tests里用fs模块写文件(Newman支持Node.js的fs):
const fs = require("fs"); const token = pm.response.json()?.data?.token; if (token) { fs.writeFileSync("./token.txt", token); pm.environment.set("authToken", token); }但这样写有个问题:Newman的沙箱环境默认不允许文件系统操作,需要在命令里加--disable-sandbox参数。而且写文件路径是相对于执行目录的,CI里要注意工作目录的设置。
5. 那些年我踩过的坑与排查链路
5.1 变量取到了但请求里是空的:作用域优先级在作怪
有一次我帮同事排查一个问题:B接口的Tests里明明console.log打印出了token,A接口的Header里也写了{{authToken}},但发出去的请求Header里token是空的。排查了半小时,最后发现是集合变量里有一个同名的authToken,值是空的。因为集合变量优先级高于环境变量,A接口取到的是集合里的空值,而不是环境里的有效值。
排查方法:在A接口的Pre-request Script里加一行console.log(pm.variables.get("authToken")),然后在Console里看输出。如果输出是空,再去检查各个作用域里有没有同名变量。Postman的变量面板(右上角眼睛图标)可以快速查看当前所有作用域下的变量值,同名变量会按优先级排列,一眼就能看出谁覆盖了谁。
5.2 提取脚本报"Unexpected token":响应不是JSON
另一个高频错误是pm.response.json()抛异常,Console里显示Unexpected token < in JSON at position 0。这个错误的意思是响应体第一个字符是<,说明返回的是HTML而不是JSON。通常是因为请求打到了错误的地址,或者接口报错返回了一个HTML错误页。
排查链路:先在Postman的响应面板里看原始响应体(Raw标签),确认返回的到底是什么。如果是HTML,检查URL是否正确、是否需要认证、请求方法是否匹配。确认接口本身没问题后,再在Tests里加Content-Type判断,避免脚本中断。
5.3 Runner执行时B接口没跑:请求被禁用了
Collection Runner界面的请求列表里,每个请求前面有个复选框。如果你不小心取消勾选了B接口,Runner就只跑A,A取不到值自然失败。这个坑很隐蔽,因为Runner不会提示"你跳过了某个请求",它只是默默地不执行。
排查方法:Runner执行前扫一眼请求列表,确认B和A都勾选了。执行后看结果面板,如果B接口根本没出现在结果里,就是被跳过了。
5.4 环境变量在团队间不同步:导出时漏了环境文件
团队协作时,你把Collection分享给同事,同事导入后跑不起来,报变量未定义。原因是环境文件没一起给。Collection和环境是分开的两个文件,导出Collection时不会自动带上环境。
解决办法:导出环境(Environments标签 → 选中环境 → Export),把JSON文件一起发给同事。同事导入环境后,在Postman右上角切换到该环境,变量就能用了。如果涉及敏感信息,导出前把值替换成占位符,让同事自己填。
5.5 token过期导致A接口401:时效性没考虑
B接口返回的token有有效期,比如30分钟。你调完B,去开了个会,回来调A,token已经过期了,A返回401。这时候你以为是提取脚本有问题,重新调B又好了,但过一会儿又不行。
这个问题的根源是token的生命周期管理。在自动化场景下,每次执行A之前都应该重新调B获取新token。用Collection Runner时,把B和A放在同一个Runner里顺序执行,每次Run都会重新调B,token就是新鲜的。手动调试时,养成习惯:调A之前先调一次B。
如果B接口调用有频率限制,不能每次都调,那就在Tests里记录token的获取时间,在Pre-request Script里判断是否过期,过期了才重新调B。但这又回到了异步调用的问题,实现起来比较复杂。我的建议是:在测试环境不要过度优化,每次调B就调B,频率限制通常不会卡测试流量。
6. 进阶:把接口依赖做成可复用的模板
6.1 用集合变量存公共配置,环境变量存动态值
一个成熟的接口集合,变量应该分层管理。公共配置(如baseUrl、appId)放集合变量,不同环境(测试、预发、生产)的差异值放环境变量,接口依赖产生的动态值(token、临时ID)也放环境变量。
这样切换环境时,只需要在右上角换一个Environment,所有请求的baseUrl和动态值都跟着变,集合本身不用改。团队协作时,集合文件共享,每个人导入自己的环境文件,互不干扰。
6.2 把提取逻辑封装成函数,减少重复代码
如果多个接口都需要从响应里提取字段,可以把提取逻辑写成一个函数,放在集合的Pre-request Script里(集合级脚本,所有请求都会执行):
// 集合级Pre-request Script pm.globals.set("extractField", function(responseJson, path) { return path.split(".").reduce((obj, key) => obj?.[key], responseJson); });然后在各个请求的Tests里调用:
const token = pm.globals.get("extractField")(pm.response.json(), "data.token"); if (token) pm.environment.set("authToken", token);但要注意,Postman的全局变量存函数在跨请求使用时有时会有序列化问题。更稳妥的方式是把函数定义在集合级脚本里,用pm.globals.set存一个字符串形式的函数体,用的时候eval。不过这样可读性差,我一般只在提取逻辑特别复杂时才这么做,简单场景直接写重复代码更直观。
6.3 用Postman的Mock Server模拟B接口,解耦依赖
如果B接口是第三方提供的,测试环境不稳定或者调用次数有限,可以用Postman的Mock Server模拟一个B接口。创建一个Mock Server,定义好返回的JSON结构,把A接口的B请求地址指向Mock Server。这样A接口的测试就不依赖真实B接口了。
Mock Server的创建入口在左侧边栏的"Mock Servers",新建后Postman会给你一个mock URL。在环境变量里把bApiUrl指向这个mock地址,A接口的Pre-request Script或前置请求就用这个地址调B。Mock Server可以配置多个示例响应,根据请求参数返回不同结果,模拟各种边界情况。
6.4 接口依赖链的调试技巧:逐段验证
当依赖链超过两个接口(A依赖B,B依赖C),排查问题会变得复杂。我的习惯是逐段验证:先单独调C,确认返回值正确;再调B,确认B能拿到C的值并正确返回;最后调A,确认A能拿到B的值。每一段都确认无误后,再用Runner串起来跑。
调试时善用Console的过滤功能。Console里会输出所有请求的日志,信息量很大。在Console顶部的过滤框里输入关键词(如"token"),只看相关日志,能快速定位问题。
7. 个人实操心得与几个小技巧
关于变量命名,我踩过一个坑:用了token这种太通用的名字,结果和Postman内置的某些变量冲突了。后来改成bizAuthToken这种带前缀的名字,再没出过问题。建议变量名加上业务前缀,比如userToken、orderId,避免和系统变量或其他集合的变量撞名。
关于脚本调试,console.log是最直接的工具,但输出太多会刷屏。我习惯用console.warn输出警告信息(黄色),console.error输出错误(红色),正常信息用console.log(白色)。在Console里按颜色快速筛选,比逐行看效率高得多。
关于环境切换,Postman右上角的环境下拉框旁边有个"Quick Look"眼睛图标,点开能看到当前所有变量的值。调接口前扫一眼,确认关键变量都有值,能避免很多"变量未定义"的低级错误。
关于集合导出,导出时选择"Collection v2.1"格式,兼容性最好。导出后可以用文本编辑器打开JSON文件,搜索变量名,确认所有依赖变量都在环境文件里有定义。这个检查在交付给团队前做一次,能省掉很多来回沟通。
最后说一个反直觉的经验:不要过度追求全自动化。有些接口依赖关系特别复杂,用脚本硬串起来维护成本很高。这种情况下,手动调B、复制值、调A反而更高效。自动化的价值在于重复执行和CI集成,如果只是一次性验证,手动操作五分钟搞定的事,没必要花两小时写脚本。工具是为人服务的,别被工具绑架。