每天对着 Postman 里几十个请求挨个点 Send,再手工把上一个接口的 token 复制到下一个接口的 Header 里,这种日子我坚持了挺久。直到有一次需要完整跑通一条“登录—下单—支付—查单”的业务链,被来回切换环境、复制参数、看返回结果搞得晕头转向,我才下定决心把请求工作流认真搭起来。跑通那一刻的感受是:Postman 这个工具,之前确实只用了个皮毛。
这篇我单独写请求工作流,适合两类读者:一种是已经能把单个接口调通、但还没尝试把多个接口串起来的人;另一种是跑 Collection 时发现请求执行顺序完全不可控、想通过脚本精细化控制流程的人。我会先把请求工作流的概念讲清楚,再拆解 Collection、变量、脚本这三个基础件,然后重点讲核心控制指令 setNextRequest 的几种实战玩法,最后分享我在真实项目中踩过的坑和一套能直接抄的回归工作流。全程用我自己的实践案例说话,不整虚的。
1. 为什么需要“请求工作流”:手工点击时代的终结
1.1 典型痛点:一个业务链要按顺序调二十个接口
大多数测试同学刚接触接口测试时,习惯是这样的:把接口按模块存在 Collection 里,调试的时候一个请求一个请求地点 Send,先从登录接口拿到 token,手动复制到下一个请求的 Authorization Header 里,再手动改几个时间戳参数,再点下一个。
单个接口这样做没毛病,问题出在流程类接口上。我接过一个订单系统的回归任务,接口数量大概二十个出头,中间还有鉴权、加签、回调通知这些依赖关系。手工点的时候,最怕的就是“前面某一步返回异常,后面全部白跑”。要么盯着响应一个个看状态码,要么中途忘记替换某处变量,整个链路从第三步开始就全错了,你还得从头再来。
这种场景下,人肉点击的本质短板暴露得很彻底:效率和准确性都指望人的注意力和手速,而这两样恰恰最不可靠。时间浪费在复制粘贴上,出错概率还高,一次回归跑下来往往比预期多花一倍时间。
1.2 工作流的本质:给请求排好队的“接力赛”
要解决这个问题,得先弄明白 Postman 里的“请求工作流”到底是什么。
Postman 本身在运行一个 Collection 时,默认行为是自上而下按顺序执行里面的请求。也就是说,你把请求按文件夹排好,Runner 就会按排列顺序逐个跑。这是“默认传送带”。而请求工作流,就是在这条默认传送带之上,通过脚本加一套控制逻辑:让哪个请求先跑、哪个请求跳过、失败之后往哪跳、最多重试几次、什么时候整个停下来。
我习惯用一个比喻来理解它:Collection 相当于一条流水线,默认只需要把工件放上传送带就能依次通过工位;但真实生产线上,传送带旁边通常还站着机械臂,机器一检测到工件“不合格”,机械臂就会把它拨到另一条检修线,或者直接扔出流水线。Postman 里的“机械臂”,就是 tests 脚本里的 postman.setNextRequest 指令。
特别提醒一点:工作流只有在 Collection Runner 执行上下文里才真正生效。你在编辑器里单独点一个请求的 Send,不存在“下一个请求是谁”这个概念,所以那些 setNextRequest 不会产生跨请求跳转效果。这一点后面我会专门讲坑。
2. 工作流的第一块地基:Collection、变量与脚本三件套
2.1 Collection 怎么组织,决定了工作流好不好写
很多人把 Collection 当成“接口收藏夹”,随便建几个文件夹就把请求往里扔。真到了要构建工作流的时候,这种随意的组织结构就会让你很痛苦。
我的经验是,Collection 的组织应该跟着“业务链路”走,而不是跟着“接口模块”走。比如支付业务,你不要只建一个“支付接口”文件夹,把支付、退款、对账、回调全塞进去;你应该按完整旅程建文件夹:下单、支付、支付结果查询、退款、对账。这样跑 Runner 的时候,默认顺序本身就接近一条真实业务链路,后续需要微调的地方也少。
还有一个特别容易被忽略的规则:一个工作流里的请求名称必须唯一。因为 setNextRequest 指令靠请求名称来定位下一个执行目标,你如果 Collection 里有两个都叫“查询订单”的请求,脚本就会变得不可预测,它可能跳到任意一个同名请求。我一般会在请求名前加业务前缀,比如“订单-创建订单”“订单-查询订单详情”。命名整齐了,工作流脚本的可读性会高很多,别人接手你的 Collection 也不会懵。
另外,请求里的域名强烈建议用环境变量占位,比如 https://{{baseUrl}}/api/order。工作流搭好后,你要跑测试环境、回归环境、预发布环境,只要切换环境变量就行,流程脚本完全不用改。这一点在后面章节会细说。
2.2 变量作用域:工作流的“数据通路”
请求工作流不只是控制执行顺序,更关键的是在请求之间传递数据。Postman 的变量系统就是这条“数据通路”。
先看作用域,从低到高优先级大致是这样:
| 变量类型 | 写法 | 生命周期 | 典型用途 |
|---|---|---|---|
| 全局变量 | {{var}} / pm.globals | 整个工作区永久存在 | 低频公共配置 |
| 集合变量 | {{var}} / pm.collectionVariables | 随 Collection 存在 | token、重复使用的业务参数 |
| 环境变量 | {{var}} / pm.environment | 随环境切换,可区分多套配置 | baseUrl、账号、密码等环境差异参数 |
| 数据变量 | {{var}}(来自数据文件) | 每次迭代临时生成 | 参数化输入 |
| 局部变量 | pm.variables | 仅当前脚本上下文 | 临时计算值 |
注意,这里说的优先级是“低优先级的同名变量会被高优先级覆盖”。比如你在全局变量里设了 username,又在环境变量里设了 username,那么请求里引用 {{username}} 时,实际取到的是环境变量里的值。
工作流里最通用的做法是:
- 环境变量只放“跟环境相关的配置”,比如接口地址、测试账号;
- 集合变量放“一次工作流运行中产生的共享数据”,比如登录返回的 token、订单号;
- 全局变量尽量少用,它太容易在团队协作时互相污染。
举个最典型的 token 传递例子。登录请求的 Tests 标签里写:
const resp = pm.response.json(); pm.collectionVariables.set('token', resp.data.token);后续所有需要鉴权的请求,Header 里直接写:
Authorization: Bearer {{token}}这样无论工作流中间加了多少个请求,只要登录在它们前面执行,token 都会自动填进去,再也不用手工复制黏贴了。
2.3 脚本的两个钩子点:Pre-request 和 Tests
Postman 脚本能干预请求的时机有两个:发送前的 Pre-request Script,和收到响应后的 Tests。理解这两个钩子的执行时机,是工作流构建的基础。
Pre-request Script 在请求真正发出去之前执行,适合做这些事:生成时间戳、计算签名、准备需要动态拼装的请求体参数、从集合变量里读取累计计数等。Tests 则在响应返回后执行,适合做这些事:断言响应结果、提取响应字段存入变量、决定下一个请求是谁。
我见过不少新人把取 token 的逻辑写在 Pre-request 里,其实是错的。token 是上一个请求的响应返回的,在上一个请求的响应没回来之前,你在 Pre-request 里怎么可能拿得到?所以“取响应数据”必须放在 Tests 里,“用数据拼下一个请求”可以放在下一个请求的 Pre-request 里,也可以直接在下一个请求的 URL/Header/Body 里用 {{变量}} 占位。
// 响应返回后:断言 + 提取 + 决定流程方向 pm.test('登录应返回成功', function () { pm.response.to.have.status(200); const data = pm.response.json().data; pm.expect(data.token).to.not.be.empty; pm.collectionVariables.set('token', data.token); });这一套下来,你就有了构建工作流的完整底座:Collection 负责存放请求和默认顺序,变量负责在请求间运输数据,脚本负责在关键时刻做决策。接下来就到了核心表演环节。
3. 核心控制器 setNextRequest:工作流的中枢指令
setNextRequest 是 Postman 工作流最核心的指令,语法非常简单:
postman.setNextRequest('请求名称');这一行代码写在当前请求的 Tests 脚本里,作用是告诉 Runner:这个请求执行完之后,下一个要去执行哪个请求。它直接覆盖默认的“按 Collection 顺序执行”的行为。
我把它拆成四种实战用法,分别覆盖大部分业务场景。
3.1 按顺序接力:最简单的线性工作流
最朴素的工作流就是让 A 请求执行完强制去执行 B 请求。比如“创建订单”跑完必须马上“查询订单详情”,而不是让 Runner 按 Collection 里乱七八糟的原始排序走。
在“创建订单”的 Tests 里写:
pm.test('订单创建成功', function () { pm.response.to.have.status(200); const orderId = pm.response.json().data.orderId; pm.collectionVariables.set('orderId', orderId); }); postman.setNextRequest('查询订单详情');这样即使 Collection 里在“创建订单”和“查询订单详情”中间还夹着其他请求,Runner 也会跳过它们,直接跳到“查询订单详情”。当然如果你的 Collection 本来就是按正常顺序排的,不写 setNextRequest 效果也一样;但显式写出来,在流程可读性和后期维护上会更清楚。
3.2 条件分支:让流程自己选择下一步
真实业务里几乎没有一条直线走到底的流程。最常见的分支场景是登录鉴权:登录成功走正常购物流程,登录失败直接跳到“清理测试数据”然后退出,避免后面二十个请求全是无效调用。
“登录”请求的 Tests 脚本可以这样写:
const resp = pm.response.json(); if (resp.code === 0 && resp.data.token) { pm.collectionVariables.set('token', resp.data.token); postman.setNextRequest('创建订单'); } else { pm.collectionVariables.set('loginError', resp.message || 'unknown'); postman.setNextRequest('清理测试数据'); }注意一个细节:setNextRequest 不一定要写在 pm.test 断言块里。你可以把决策逻辑放在断言外面,这样即使用户手动点 Send,也不影响脚本执行。不过运行 Runner 时,整个脚本都会执行,所以是否放在断言里其实不影响结果。
条件分支在权限类场景里特别实用。比如有一个需要管理员权限的接口,如果当前账号权限不足,响应返回 403,你就可以直接跳到“切换管理员账号”请求,再重新调一次目标接口。这相当于用脚本做了自动化容错。
3.3 循环重试与轮询:把“稍后再试”交给脚本
有些接口不是同步返回最终结果的,典型的是异步任务:提交任务后立刻返回“处理中”,真正结果要等几秒甚至几十秒才好。手工测试这类接口时,你只能反复点同一个请求,盯到结果出来为止。工作流完全可以把这个过程自动化。
基本思路是:用同一个请求轮询结果,每轮在集合变量里累加计数,满足条件就跳出去,不满足就继续跳回自己。
“查询任务结果”请求的 Pre-request Script:
let count = pm.collectionVariables.get('pollCount'); count = count ? count + 1 : 1; pm.collectionVariables.set('pollCount', count);这个请求的 Tests 脚本:
const resp = pm.response.json(); const done = resp.data.status === 'SUCCESS'; const count = pm.collectionVariables.get('pollCount'); if (done || count >= 5) { // 任务完成,或重试次数耗尽,进入收尾请求 pm.collectionVariables.set('pollCount', 0); // 计数器复位,重要! postman.setNextRequest('生成最终报告'); } else { postman.setNextRequest('查询任务结果'); }这里有两个关键点,提前说清楚:
- 计数器复位。如果第 5 次轮询结束后你不复位 pollCount,下一次整套工作流跑的时候,计数还会从旧值继续累加,导致重试逻辑失效。这就是典型的全局状态污染问题。
- setNextRequest 循环是“请求级别的循环”,它只影响当前这次 Runner 迭代内部的走向。如果你配置了 3 次 iteration,那么每个迭代里这个循环都会独立执行一遍。它和数据驱动的迭代是两个不同维度的概念,别混在一起。
3.4 提前终止与异常退出:及时止损
如果你判断当前请求的响应已经“神仙也救不回来了”,继续跑下去只会浪费时间和资源,那就直接终止这次迭代。方法很简单:
postman.setNextRequest(null);null 的作用是结束当前迭代。在“登录失败”直接跳出来、或者在关键数据校验不通过时马上停掉后面一连串请求,都非常有效。
但注意一个容易误解的地方:setNextRequest(null) 只是终止当前这条迭代链路,并不会让整个 Runner 停下来。如果你用数据文件跑了 20 条数据,某一条数据走到一半终止了,Runner 依然会继续执行下一条数据的流程,只是当前这条不再往下走了。想要数据驱动的某次迭代失败就整体停止,得在 Runner 设置里勾选“遇错停止”之类的选项,或者在每个请求的 Tests 里让断言主动失败。
4. 把数据驱动接进工作流:Data File 与迭代执行
工作流解决的是“流程怎么走”的问题,数据驱动解决的是“同一套流程怎么用不同数据走多遍”的问题。把两者结合起来,才能真正发挥 Postman 在回归测试里的威力。
4.1 CSV/JSON 数据源怎么接入
在 Runner 界面里,你可以选择上传一个数据文件,Postman 会按照文件里的内容生成多组测试数据,每一行数据对应一次 iteration。
数据文件格式最常见的是 CSV 和 JSON 两种。CSV 长这样:
username,password,expectAmount testuser01,pass123,99.00 testuser02,pass456,129.00JSON 长这样:
[ {"username": "testuser01", "password": "pass123", "expectAmount": 99.00}, {"username": "testuser02", "password": "pass456", "expectAmount": 129.00} ]上传数据文件后,请求里引用数据变量直接用双大括号:URL 里写 {{username}},Body 里写 {{password}}。脚本里读取数据变量用 pm.iterationData.get('username')。
有个细节值得注意:数据文件里的变量只存在于当前这次迭代,下一次迭代会有下一行数据。所以数据变量适合放那些“每轮都不同”的输入参数;而 token、订单号这类“一轮内共享、下一轮重置”的产物,应该放集合变量,在当前迭代的 Tests 里写入,然后靠下一请求读取。
4.2 迭代过程中的变量隔离与上下文维持
我见过最典型的数据驱动翻车场景是这样的:某人用 10 行数据跑同一个工作流,发现第 2 轮开始 token 始终是第 1 轮的旧值,于是第 2 轮居然用旧 token 跑完了全流程。问题出在变量写入的位置:他把 token 写入集合变量后,下一轮迭代登录失败,集合变量里的 token 没有更新,但后续请求依然读取了这个旧 token。
正确的做法是,登录请求的 Tests 里要同时做到三件事:
// 1. 不论成功失败,先把旧 token 清掉,避免残留 pm.collectionVariables.set('token', ''); // 2. 拿到新 token 就写入 if (pm.response.code === 200) { const resp = pm.response.json(); if (resp.data && resp.data.token) { pm.collectionVariables.set('token', resp.data.token); } } // 3. 登录失败直接终止当前迭代 if (!pm.collectionVariables.get('token')) { postman.setNextRequest(null); }这样每轮迭代都是干净的:先清空、再尝试写入、失败就及时止损,不会让上一轮的脏数据带到下一轮去。
4.3 结果检查与导出报告
数据驱动工作流跑完之后,第一步是看 Runner 面板里的统计。Postman 会对每个请求的每个断言打勾或打叉,一眼能看出哪一行数据失败在哪一个环节。不过仅靠界面看报告,对大量数据来说还是不够爽。实际项目里我更推荐用 Newman,也就是 Postman 的命令行版本。
newman run my-collection.json -e test-env.json -d testdata.csv -r cli,htmlextra它跑出来的 HTML 报告会详细列出每次迭代、每个请求、每个断言的通过情况,还可以直接集成到 CI 流程里。如果你的团队已经有定时任务或者流水线,让工作流每天自动跑一遍、报告自动归档,这套方案会非常省心。
5. 五种常见的“工作流翻车”现场与排查链路
工作流这东西,写完跑通就是“真香”,写的过程中却总能遇到各种莫名其妙的问题。下面五个坑是我和身边同事踩得最多的,我把排查链路一起写出来,你遇到类似问题可以直接照着查。
5.1 setNextRequest 指向的请求名不匹配
现象:Runner 执行某个请求后,下一个请求没有按预期执行,有时整个 Runner 只跑了一个请求就结束了,而且没有明显报错。原因是 setNextRequest 里写的请求名称和 Collection 里的实际请求名对不上,大小写、空格、多一个字符都不行。
排查链路:
- 打开 Collection 列表,逐个核对脚本里的请求名称与实际请求名称是否一字不差;
- 注意 Postman 的请求名是区分大小写的,“查询订单”和“查询订单 ”(末尾多一个空格)是两个名字;
- 推荐做法:不要手打名称,直接从 Collection 里复制请求名到脚本字符串里,最大限度避免误差。
5.2 上一请求的变量到下一请求变成 undefined
现象:请求 Header 里明明写了 {{token}},跑起来却变成 undefined,或者带着 undefined 去请求导致 401。这个坑的原因很多,最常见的是三个。
排查链路:
- 先用 console.log 在写入方和读取方分别打印变量值,确认写入是否成功、读取的 key 是否一致;
- 检查变量作用域冲突:集合变量里有个同名 token,环境变量里也有,而环境里那个是空值,优先级高的空值把集合变量覆盖了;
- 检查执行顺序:如果你单独点过登录请求,再单独点下单请求,{{token}} 当然是 undefined,因为单点登录拿到 token 后,只在集合变量里写了值,你单点下单请求时它并不会先去执行登录。请务必把整个流程放进 Runner 里跑。
5.3 循环重试变成死循环或永不重试
现象:轮询请求没完没了地跑,Runner 彻底卡死;或者明明没到成功状态,却跳出了循环直接进入收尾请求。
造成死循环最常见的原因是计数器变量在每轮请求里被重新初始化。比如你把计数逻辑写在了 Tests 里,而每轮都 setNextRequest 跳回自己,看似在累加,其实每轮进入时 Tests 重新读到的值并不是上一轮写进去的——注意,如果写计数和读计数用的是不同的键名,或者 Pre-request 和 Tests 里都有赋值语句互相覆盖,计数器会始终停留在一个值上。
排查链路:
- 在 Pre-request Script 里打印当前计数,确认每轮都在增加;
- 确认计数器的读写都使用同一个键名,比如统一用 pollCount;
- 给循环加硬上限,比如 count >= 5 就无条件跳出,这相当于防呆保护,让死循环最多跑 5 次而不是无限次;
- 检查是否在循环结束分支里复位了计数器。不复位的后果是下一次完整流程的初值不对,看起来像“永不重试”或“只重试一次”。
5.4 CSV 数据文件读出来后字段名带 BOM 或中文乱码
现象:数据文件只有两列,脚本里 pm.iterationData.get('username') 却一直返回 undefined;或者响应断言里出现了奇怪的 \ufeff 前缀。
原因是 Windows 下用记事本或 Excel 另存的 CSV 通常是带 BOM 的 UTF-8 编码,Postman 读进来后,第一列列名会被解析成“\ufeffusername”,你按 username 去取自然取不到。中文乱码也是编码问题。
排查链路:
- 在 Pre-request 里打印 pm.iterationData.toObject(),看看实际读取的键名和值;
- 如果看到 \ufeff,把 CSV 用专业的文本编辑器另存为“UTF-8 without BOM”格式;
- 或者干脆改用 JSON 数据文件,编码问题少得多,变量名也更直观。
5.5 在单个请求里调试 setNextRequest,发现完全不生效
现象:在某个请求的 Tests 里写了 postman.setNextRequest('下单请求'),然后直接点 Send,Postman 什么反应都没有,甚至有时候提示脚本执行完但没有后续动作。你以为是脚本写错了,其实不是。
原因很简单:setNextRequest 的设计目标是给 Collection Runner 用的。在单个请求的 Send 模式下,Runner 根本没有启动,没有“下一个请求”的上下文,这条指令自然没有任何效果。
排查链路:
- 别在单请求模式里验证工作流逻辑,要验证就建一个只含两个请求的最小 Collection,用 Runner 跑一遍;
- 如果只想快速看某个逻辑分支,可以在 Tests 里临时加 pm.test 来验证条件判断是否正确,但最终确认必须在 Runner 中进行。
6. 一份可以直接抄的回归工作流:从三小时到六分钟
讲了这么多原理和踩坑,最后分享一个我实际搭建并一直在用的回归工作流模板,你可以直接拿过去改。
6.1 搭建步骤概述
场景是某个订单系统的全链路回归,包含登录、创建订单、支付、查询订单、退款、对账、清理数据,总共七个主要请求。我在 Collection 里按这个顺序排列,每个请求前都加了业务前缀,保证名称唯一。
登录请求的 Tests 里做了三件事:断言状态码、提取 token 写入集合变量、失败时清空 token 并 setNextRequest(null)。后面五个业务请求的 Tests 里都做了对应的响应结构断言:断言业务码为 0、断言关键字段存在、断言金额等于预期值。最后一个“清理数据”请求放在所有链路的终点,不管前面成功走到哪一步,只要主链路走完就必然进入清理请求,这样每次回归结束后测试数据都是干净的。
跑一次完整的全链路回归,数据文件里放 10 组用户数据,Runner 勾选了“按数据行数迭代”,加上失败自动停止的选项。效果是:原来人工点完二十多个接口、手工核对返回结果、还要清理数据,大概要三小时;现在六分钟跑完,还能拿到一份带断言结果的详细报告。
6.2 维护成本和团队共享
工作流搭建不是一劳永逸的。接口入参变了、返回字段结构调整了、新增了必填字段,都需要同步修改对应请求和脚本。我的维护习惯是:写脚本时在关键分支旁边加注释,说明为什么这么跳;请求名称变更时全局搜索一遍 setNextRequest 引用,避免漏改。
团队共享 Collection 时,有两条红线:一是不要把登录密码、密钥这类敏感信息直接写进集合变量或请求体里,统一用环境变量承载,不同成员各建各的环境;二是共享前在 Runner 里完整跑一遍,确认没有依赖某个人本地环境才有的数据。这样团队里任何人拉下 Collection,配上自己的环境变量,都能直接跑通整套工作流。
我自己现在的基本操作是:先搭一条两条请求的最小链路,验证 setNextRequest 和变量传递逻辑没问题,再逐步往里加请求、加分支、加数据驱动。每次改完流程,都先清空 collectionVariables 再跑一轮,确保没有旧数据残留。这套工作流是我做接口测试以来用得最顺手的工具组合,希望对你也有帮助。