Postman接口串联:环境变量与Pre-request Script实战指南
我前阵子做一个多接口联调的需求,遇到一个特别典型的场景:登录接口的 token 还没拿到,后面的业务接口已经开始报 401 了;下单要用的商品 ID 得从上一个列表接口里现取,结果换到测试环境,所有前置数据又要重新造一遍。一开始我以为接口写得有问题,排查到最后才发现,真正的问题在于接口之间的数据依赖完全没有被管理起来。接口串联的骨架,其实就是“环境变量”和“Pre-request Script”这两样东西。环境变量负责把运行时数据放到所有请求都能读到的位置,Pre-request Script 则负责在每个请求真正发出去之前,把该准备的参数、签名、前置数据都准备好。
这篇文章特别适合正在做接口测试、接口联调、自动化脚本维护的人,尤其是已经接触过 Postman 但总觉得脚本越写越乱的同学。把这两个机制用明白以后,接口依赖再复杂也能串得整整齐齐,而且换环境、换账号、重新跑数据都只是“切换一下”的事。
1. 接口串联的本质:数据依赖的三个层次
很多人一听到“接口串联”,第一反应就是把多个请求按顺序点一遍。但真正串联的核心不是“顺序”,而是“数据依赖”。一个接口的输出,往往是另一个接口的输入;一个请求的签名,可能依赖请求体里的多个字段。想把这套东西管理好,先要看清楚数据依赖到底发生在哪几个层面。
1.1 同一请求内部的自引用
同一个请求内部,URL、请求头、请求体之间经常出现重复数据。比如一个查询接口,URL 上要带 timestamp=1712000000,请求体里也要带同样的 timestamp,签名串里还要用它。如果直接在三个地方写死,改动就是全局替换,而且很容易改漏。
我在实际项目中会用变量把这些“共享值”先定义出来。比如在 Pre-request Script 里生成 timestamp 和 nonce,然后 URL、Header、Body 全部通过 {{timestamp}} 和 {{nonce}} 引用。这样不仅改起来方便,更重要的是整个请求的上下文是自洽的——签名、时间、随机数一定来自同一份数据,不会出现“Header 里是一个时间,Body 里是另一个时间”这种低级错误。
1.2 不同接口之间的数据传递
这是最常见、也最让新手头疼的场景。A 接口返回了 token,B 接口的请求头要带上它;C 接口返回了订单号,D 接口的 URL 路径上要用它。如果靠人工复制粘贴,跑一次就要手动处理一次,环境一换就全乱套。
正确做法是:在 A 接口的 Tests 脚本里,从响应中提取数据并写入环境变量;在 B、C、D 接口的 Pre-request Script 或请求参数中,读取这些变量。数据一旦进入环境变量,就等于进入了公共区域,任何一个请求都能读到,也能被后续请求覆盖更新。这是接口串联最核心的数据通路。
1.3 跨环境的切换与迁移
开发环境、测试环境、预发布环境,域名不一样,账号不一样,甚至返回的数据结构都可能不一样。很多团队的做法是复制整个集合,然后逐个改 URL,改完就完。这种做法的维护成本非常大——上游接口一变,所有环境下的集合都要同步。
用环境变量来解决这个问题,原理其实很简单:把 baseUrl、账号、密码等可能随环境变化的量,全部提取成变量,放进 Environment 里。每个环境一套变量,请求里只写 {{baseUrl}} 这种引用。切换环境时,只需要在右上角换一个 Environment,整套集合的数据依赖就跟着切换了。这个思路看起来简单,但真正做到位的团队,接口集合的维护成本会低一个量级。
2. 环境变量:它不是你想的那种“全局变量”
系统里常说的“环境变量”是操作系统层面的 PATH、JAVA_HOME 之类的配置,但接口工具里的环境变量完全是另一回事。Postman 里的 Environment 是一组 key-value 对,可以在不同的配置环境之间切换。很多人对它有个误解:以为环境变量就是“全局变量”,存进去到处能用。这个理解只对了一半,变量体系里至少有五类变量,它们的作用域和优先级是完全不同的。
2.1 五类变量的分工与优先级
我把 Postman 里常见的五类变量整理成了表格,方便直观对比:
| 变量类型 | 作用域 | 写入方式 | 典型用途 |
|---|---|---|---|
| 全局变量 | 整个用户级别,跨集合可用 | pm.globals.set("key", value) | 所有集合共用的默认配置 |
| 环境变量 | 当前选中的 Environment | pm.environment.set("key", value) | 不同环境各自的域名、账号、token |
| 集合变量 | 当前集合内可用 | pm.collectionVariables.set("key", value) | 集合内部共享的固定配置 |
| 数据变量 | Runner 数据文件中的字段 | 外部 CSV/JSON 注入 | 批量跑测试时的用例数据 |
| 局部变量 | 当前请求运行过程中 | pm.variables.set("key", value) | 请求内临时中间值 |
这里要特别注意优先级。Postman 解析变量时,同名变量的生效顺序是:局部变量 > 数据变量 > 环境变量 > 集合变量 > 全局变量。也就是说,如果环境变量里有一个 token,集合变量里也有一个 token,实际生效的是环境变量里的那个;如果你在脚本里用 pm.variables.set("token", "local") 设置了一个临时变量,那么本请求内所有 {{token}} 都会解析成这个临时值,而不是环境变量里的值。
这个优先级机制用好了是利器,用不好就是深坑。我见过一个同事排查了半天,发现脚本里读到的值和自己设置的环境变量不一致,最后发现是集合变量里一个同名旧值把环境变量给“覆盖”了。所以排查变量问题时,第一件事就是确认当前到底选中的是哪个 Environment,这个变量在各层里有没有重名。
2.2 initial value 和 current value 的设计意图
环境变量面板里每个变量都有两个值:初始值(Initial Value)和当前值(Current Value)。很多人直接无视这个区别,但理解了之后对团队协作很重要。
初始值会随着集合的分享、同步一起分发出去,相当于“公开的默认值”;当前值只存在于本机,是“运行时实际使用的值”。举个例子,登录接口的脚本会自动把 token 写入环境变量,那么 token 的当前值会被更新成最新 token,但初始值始终为空或者一个占位值。这样把集合分享给同事时,token 的初始值不会泄露,同事拉下来后自己跑一遍就能生成本机的当前值。
这里有个我反复强调的操作原则:不要把密码、密钥写进初始值。初始值是会跟着集合一起同步的,一旦同步到公共仓库,就等于明文泄露了。正确做法是把这些敏感信息留空,靠 Pre-request Script 动态生成或运行时手动输入。
2.3 变量读取不到的通用排查思路
如果遇到 {{token}} 解析不出来,我一般按这个顺序排查:
- 右上角 Environment 是否选中了正确的环境;
- 变量名是否拼错,尤其是大小写和首尾空格;
- 是否有更高级别的同名变量把目标变量覆盖了;
- 变量是不是在脚本运行后才写入的,请求发出时还没生成;
- 当前值是否为空,初始值是否被清空。
这几步走完,95% 的变量读取问题都能定位。剩下的 5% 大多是踩了脚本时序的坑,下一章详细说。
3. Pre-request Script:请求发出前的那一刻能做多少事
Pre-request Script 是在请求发送之前执行的一段 JavaScript。它的定位,就是在“请求发出前”这个时间窗口把请求所需的动态数据准备好。很多教程把它简单说成“生成个时间戳”“算个签名”,实际上它的作用远不止这些。
3.1 执行时机与变量渲染顺序
先搞清楚时间线。在 Postman 里,你点击 Send 之后,执行顺序大致是:
- 加载当前选中的环境变量、集合变量和全局变量;
- 执行当前请求的 Pre-request Script;
- 执行集合级 Pre-request Script(如果配置了);
- 渲染并发送请求(解析 URL、Header、Body 中的 {{变量}});
- 收到响应后执行 Tests 脚本。
也就是说,Pre-request Script 和变量渲染都发生在“请求发出前”,脚本先跑,然后再渲染变量。所以理论上,在 Pre-request Script 里设置的环境变量,在当前请求的 URL 或 Body 中引用是能生效的。
但这里有一个特别迷惑人的现象:如果你在脚本里用 pm.environment.set("timestamp", new Date().getTime()),然后回到 URL 编辑框,看到 {{timestamp}} 的“已解析”值可能还是上一次的旧值。这不是脚本没生效,而是界面上的解析结果没有实时刷新。要验证脚本到底有没有用,就在脚本里 console.log(pm.environment.get("timestamp")),打开 Postman 控制台看输出,比盯着编辑器里的提示靠谱得多。
3.2 动态生成请求参数
这个用途最直观。常见需求有:
- 生成当前时间戳:
Math.floor(Date.now() / 1000) - 生成请求唯一 ID:
"REQ_" + Date.now() + "_" + Math.floor(Math.random() * 10000) - 对请求参数做 MD5/SHA256 签名,Postman 内置了 CryptoJS:JAVASCRIPTconst CryptoJS = require("crypto-js");const data = "param1=value1¶m2=value2&key=secret";const sign = CryptoJS.MD5(data).toString().toUpperCase();pm.environment.set("sign", sign);
- 生成 UUID 或随机字符串,可以用
pm.variables.replaceIn("{{$guid}}")或者自己拼。
这些动态值如果手动生成,不仅慢,而且容易产生“不同请求用了同一份随机数”的问题。放到 Pre-request Script 里自动生成,每次请求都是独立的动态参数,更符合接口的真实调用场景。
3.3 在 Pre-request Script 里再发起请求
这是很多人没用起来的功能:Pre-request Script 里可以通过 pm.sendRequest 再发一个请求。这意味着,一个请求可以在真正发出前主动去拉取前置数据,而不只是从环境变量里读现成的值。
典型场景是自动获取 token。假设我的主请求需要 token,但我希望每次跑之前自动拉一次,而不是手动先跑登录接口。那就在主请求的 Pre-request Script 里写:
这里有个必须注意的坑:pm.sendRequest 的回调是异步的。Postman 会等这个回调完成后再继续发送主请求吗?答案是,如果主请求引用了 token 变量,虽然理论上回调结束后变量才被写入,但实际运行中脚本可能会提前继续。稳妥的做法是在回调里把所有依赖逻辑都处理完,或者用同步方式避免竞态。我自己更倾向于把 token 获取做成独立的登录请求,通过 Runner 按顺序执行,而不是塞在每个请求的 Pre-request Script 里,后者的时序控制会让人抓狂。
4. 一个完整的接口串联案例:登录-下单-支付
讲完原理,直接上一个实际会遇到的业务场景。假设我正在测试一个电商下单流程:登录接口获取 token,创建订单接口返回订单 ID,支付接口用订单 ID 去发起支付。这三个接口之间有两层数据依赖:token 和 orderId。
4.1 业务场景与数据流设计
我先把数据流向画在脚本规划里:
- 登录接口:输入 username、password,输出 token;
- 创建订单接口:请求头带 token,请求体里带 orderNo,输出 orderId;
- 支付接口:URL 路径或请求体带 orderId,请求头带 token。
基于这个分析,环境变量需要预置这些 key:baseUrl、username、password、token、orderNo、orderId。
注意 token 和 orderId 这类运行时数据,在环境变量面板里初始值留空,由脚本动态写入。baseUrl、username、password 这种配置类数据,初始值直接填好,方便切换环境。
4.2 登录接口:用 Tests 把 token 写入环境变量
登录接口本身不需要特殊的 Pre-request Script,主要逻辑放在 Tests 里,从响应中提取 token 并写入环境变量:
这里我习惯先做断言再赋值。只有响应里确实包含 token 才写入环境变量,否则赋值一个空串,后面所有依赖请求都会静默失败,排查成本反而更高。
4.3 创建订单接口:Pre-request Script 准备数据
创建订单接口的 Pre-request Script 做三件事:生成唯一订单号、确保请求头带上 token、把订单号暂存到环境变量,方便后面校验用。脚本如下:
然后在请求体里直接引用变量:
这里我特别解释一下“为什么在 Pre-request Script 里手动往 Header 里塞 token,而不是直接在 Header 编辑框里用 {{token}}”。两种方式都可以,但我推荐后者更简单——直接在 Header 栏写 Authorization: Bearer {{token}}。手动写脚本的好处是可以在写入前做逻辑判断,比如 token 不存在时自动中断请求或者打印清晰日志;坏处是增加了代码量。实际项目中,我会把“读取并校验变量”这类逻辑统一封装成集合级脚本,避免每个请求重复写。
4.4 创建订单接口的 Tests:把 orderId 存进环境
订单创建成功后,响应里会返回 orderId。在 Tests 中写入环境变量:
这样从登录到创建订单,token 和 orderId 已经进入了环境变量,支付接口可以直接消费。
4.5 支付接口:读取 orderId 和 token
支付接口的 Pre-request Script 非常轻量:
通过抛出异常的方式,强制约束了执行顺序。如果用户直接跳过来跑支付接口,脚本会明确提示少了哪一步,而不是发送一个注定失败的请求。
支付请求的 URL 用变量拼接:
或者用路径参数方式,在 URL 里写 {{orderId}},具体取决于个人习惯。我一般会把 URL 设计成 /api/pay/{{orderId}} 这种带路径参数的风格,脚本职责更单一。
这套串联跑通以后,整个流程就是:跑登录接口 → token 自动写入 → 跑创建订单接口 → orderId 自动写入 → 跑支付接口 → 用最新的 orderId 发起支付。中间不需要手动复制任何数据。
5. 实操中容易踩的坑:时序、为空与并发
接口串联的脚本写起来不难,难的是跑着跑着突然不灵了。下面这几个问题,是我在实际项目中真实踩过、也在帮同事排查时高频遇到的。
5.1 变量为空时不报错,只会静默变成空串
这是新手最容易困惑的一点。如果 {{token}} 这个变量不存在或者值为空,Postman 解析后的 URL 不会报错,而是直接把变量部分替换成空字符串。比如 /api/pay/{{orderId}} 会变成 /api/pay/,请求还是会发出去,然后收到 404 或者 400,看起来像“接口有问题”。
解决思路就是我在支付接口里演示的做法:在关键请求的 Pre-request Script 里主动判断变量是否为空,为空就抛异常或打日志。宁可让脚本停下来,也不要让它带着空参数去请求服务器。
5.2 脚本里 set 的变量,本请求的 UI 显示旧值
我在前面提到过,Pre-request Script 执行完以后,Postman 才会解析请求中的变量引用。但开发工具界面上显示的“已解析值”经常是上一次运行的结果,导致很多人误以为“脚本设置完环境变量之后,本请求内引用不了”。
判断这个问题最有效的方法是打开 Postman 控制台(下方 Console),点击请求发送后,控制台会打印出实际请求的 URL 和 Headers。这里面显示的是最终真正发出的请求,所有的变量都已解析完毕。以控制台为准,别太相信编辑器面板里的预览。
5.3 并发执行时环境变量互相覆盖
Postman Runner 支持多接口并发,但环境变量是一个共享存储,多个请求同时往同一个 key 里写值,最后谁写入谁说了算,但顺序不可控。典型问题是:两个请求同时用 Pre-request Script 往 token 里写入数据,结果先请求的 token 被后请求的 token 覆盖,先请求的响应校验就出问题。
应对思路有三种:
- 尽量让写接口串行执行,读接口可以并发;
- 对共享写操作的 key,单独抽到一个初始化请求里,按顺序先跑完;
- 如果并发场景无法避免,用局部变量 pm.variables.set 而不是环境变量,让每个请求持有自己的临时副本。
5.4 特殊字符与 URL 编码问题
把响应中的值直接拼到 URL 里,很可能踩到编码坑。比如 token 里带 +、/、=,密码里带 &、#,这些字符在 URL 里有特殊含义,直接拼接会导致请求参数解析错误。
我的习惯是:凡是可能包含特殊字符的数据,在拼进 URL 之前先用 encodeURIComponent() 编码;如果是拼 JSON 请求体,则避免直接字符串拼接,尽量借助 JSON.stringify 构建对象再转字符串。
Header 里带 token 一般不需要编码,URL 的 query 参数里带这类值时必须编码。具体哪些地方编码,取决于目标接口的约定,不是无脑全编。
5.5 环境切换后旧数据残留
切换到另一个环境后,如果新环境里没有预置 token、orderId 这些变量,请求可能会读到上一个环境留下的旧值,导致拿到的是另一个环境的订单号,又定位了半天才发现是环境变量没清干净。
我现在的做法是:给每个环境预置一把“初始化”请求或一个集合级脚本,在进入新环境后先清理运行时变量,把 token、orderId 这类动态变量 unset 掉,确保从零开始。
这个清理动作也可以放到集合的“到期”事件里,但最直观的还是手动先跑一遍清理脚本,跑完再执行业务流。
6. 几个让脚本从“能跑”到“可维护”的习惯
最后聊点工程层面的经验。脚本跑通容易,维护难。尤其是一个集合里有几十个请求、每个请求都带一段脚本的时候,没有规范就会变成一团乱麻。
6.1 统一变量命名规范
我用的命名风格是 camelCase,且按“前缀_业务含义”分组。比如 baseUrl、adminToken、shopOrderId。不同功能的变量不要混用,例如 token 这个命名就太宽泛了——我见过有人把登录 token 和第三方回调 token 都叫 token,最后覆盖到怀疑人生。建议至少在命名上区分业务来源。
6.2 用 console.info 留下关键日志
脚本执行过程对使用者来说是黑盒,一旦出问题,最好的排查方式就是看日志。我会在每个关键节点加 console.info,例如:
这些日志在 Postman Console 里一目了然,配合请求流程能快速定位是脚本问题、数据问题还是接口问题。
6.3 把通用逻辑下沉到集合级脚本
一段逻辑如果多个请求都在用,就放到集合级别的 Pre-request Script 或 Tests 里。比如全局签名逻辑、公共请求头的注入、token 过期自动重试等。集合级脚本会在集合里每个请求执行前/后运行,这样单个请求的脚本只需要关注自身特有的逻辑,重复代码量大幅下降。
6.4 与 CI/CD 结合时,环境变量从外部注入
跑自动化测试时,环境变量不一定写在 Postman 里,也可以从外部注入。Postman Runner 和 Newman 都支持通过环境文件(JSON 格式)导入环境变量。这样在 CI/CD 里,可以针对不同分支传入不同的 baseUrl 和账号,做到测试环境、预发布环境、生产环境互不干扰。
我在项目里经常用 Newman 跑接口回归,命令大致是:
test-env.json 里就是环境变量集合。这个文件的变量值由 CI 流水线在部署时动态生成,代码仓库里只需要保留模板,不提交真实密码。
6.5 一个值得养成的最后习惯
使用这套机制一年之后我最大的体会是:接口串联的稳定性,不取决于脚本写得多花哨,而取决于骨架搭得多清晰。环境变量管数据共享,Pre-request Script 管请求准备,Tests 管数据抽取,各司其职。新接手项目的人只需要打开集合,看一眼环境变量面板里的变量命名,再看一眼关键请求的脚本日志,就能知道整个数据链路是怎么走的。
如果让我再分享一个小技巧,那就是在每次跑完一个完整流程后,花十分钟把 Console 里的日志整体过一遍,删除无用的调试输出,补上缺失的关键日志。这些时间看着不起眼,等到某天线上环境联调出问题、需要快速定位数据链路时,你会非常庆幸当初做了这个整理。