ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Postman公共函数封装指南:告别Pre-request Script重复代码

Postman公共函数封装指南:告别Pre-request Script重复代码 做接口联调这几年我在 Postman 里最烦的事不是接口长时间无响应而是同一个签名算法在十几个请求的 Pre-request Script 里各放了一份。每次后端改一点逻辑我都要打开每个请求、找到那段一模一样的代码、逐个替换还得提心吊胆怕漏改某一个。后来痛定思痛决定把公共函数统一抽出来。这篇文章就把我折腾 Postman 定义公共函数的完整经验写出来包括三套不同的实现方案、每一套背后的原理以及实测中踩过的各种坑。如果你也经常被复制粘贴脚本折磨这篇应该能帮你省下大量时间。1. 为什么接口测试里总是重复造轮子1.1 一个再常见不过的崩溃瞬间Postman 脚本的重复和业务代码里的重复不太一样。业务代码你还能抽一个 utils 文件到处 importPostman 早期版本没有一个真正意义上的公共模块入口。于是很多人的做法是某个请求脚本里写好了一段签名逻辑测试通过了下一个请求直接复制再下一个请求继续复制。等到整个 Collection 里铺满了同一段代码噩梦才开始。我之前接手过一套接口测试集合三十多个请求每个请求的 Pre-request Script 里都有一段 HMAC 签名代码代码内容一模一样只是复制时有人多粘贴了一次、有人少了一行。后端在一次安全升级中把签名算法从 HMAC-SHA1 换成了 HMAC-SHA256我花了一个晚上把三十多个请求逐个打开、逐段替换中间还漏掉了三个导致第二天联调时同一个错误被连续报了四五次。那之后我意识到在 Postman 里定义公共函数不是代码洁癖而是接口测试工程化的基本前提。只要你的接口数量超过五个、或者同一段脚本被粘贴超过两次就应该考虑这个问题了。1.2 先理解 Postman 脚本的执行顺序和变量体系要选对公共函数的方案得先弄清楚 Postman 的脚本执行链路。一个请求从发送到结束脚本的执行顺序大致如下执行阶段脚本位置用途第一阶段Collection 级 Pre-request Script集合内所有请求都会先执行适合放全局公共逻辑第二阶段Folder 级 Pre-request Script当前文件夹内的请求先执行适合放文件夹级公共逻辑第三阶段Request 级 Pre-request Script当前请求自身携带的脚本第四阶段发送请求本身无脚本执行第五阶段Request 级 Tests当前请求收到响应后执行的断言第六阶段Folder 级 Tests文件夹级公共断言第七阶段Collection 级 Tests集合级公共断言也就是说Collection 级 Pre-request Script 天然适合作为公共代码区它位于所有子请求之前定义在这里的函数和变量后续的请求脚本、Tests 脚本都能访问到。这一点是 Postman 定义公共函数最核心的基础。除了执行顺序Postman 的变量体系也很关键。它至少包含五层变量全局变量、环境变量、集合变量、数据变量、局部变量。我们在做公共函数时主要会用到前三种因为公共函数需要读取配置的值而配置往往存在于环境变量或集合变量中。理解这些变量怎么传递后面几套方案才不会选错。2. 方案一把函数字符串化存进全局变量再用 eval 唤起2.1 最朴素也最普及的操作步骤这是网上流传最广、也是老版本 Postman 时代就存在的方案。核心思路是把一段 JavaScript 代码当作字符串存进全局变量或环境变量在脚本顶部用eval()执行这段字符串于是函数就活了。先看第一步。在 Postman 左侧菜单进入 Environments选 Globals新建一个变量名字建议叫utils值填一段字符串化的 JavaScript 代码。比如function genTimestamp() { return Math.floor(Date.now() / 1000); } function genNonce(len) { var chars abcdefghijklmnopqrstuvwxyz0123456789; var nonce ; for (var i 0; i len; i) { nonce chars.charAt(Math.floor(Math.random() * chars.length)); } return nonce; }然后在任意请求的 Pre-request Script 里写eval(pm.globals.get(utils)); // 这句之后genTimestamp 和 genNonce 就能直接用了 var timestamp genTimestamp(); var nonce genNonce(16); console.log(timestamp, nonce);这样凡是想用这两个函数的请求都只需要一行eval再加调用不用再复制一整套函数体。要改逻辑只需要改全局变量utils里的字符串所有引用了eval的请求下次执行时都会拿到新版本。2.2 eval 方案背后的作用域原理很多人担心eval的性能和安全性。这里要说明一下Postman 的脚本运行在一个独立的沙箱环境中每次请求执行时Collection 脚本、Pre-request 脚本、Tests 脚本共享同一个全局作用域。eval执行的代码本质上就是在当前全局作用域中解释运行。所以eval(pm.globals.get(utils))执行完函数声明genTimestamp就会挂到全局对象上后面再调用自然找得到。也正因为共享全局作用域eval方案有一个容易翻车的细节函数声明和var声明会暴露到全局但const和let不会。换句话说如果你的全局变量里写的是const genTimestamp () Math.floor(Date.now() / 1000);那么eval之后你直接调genTimestamp()大概率会报genTimestamp is not defined。我在实测里就栽过这个跟头。要稳妥公共函数的字符串里请尽量使用function声明式写法或者用命名空间对象挂载var ApiUtils {}; // 用 var ApiUtils.genTimestamp function() { ... }; ApiUtils.genNonce function(len) { ... };执行完eval后通过ApiUtils.genTimestamp()调用只要ApiUtils是var或全局对象上已有的属性就不会有作用域丢失的问题。2.3 eval 方案的短板字符串转义和代码管理这个方案虽然简单但它的短板也很明显。第一是字符串转义。如果你把上面那段函数体原样复制到 Globals 变量的输入框里可能因为引号、换行、反引号等各种问题导致解析出错。我的经验是别直接在变量值里反复改可以先在一个临时请求的脚本里写好函数源码然后利用JSON.stringify把它变成字符串输出再复制进 Globalsfunction genNonce(len) { var chars abcdefghijklmnopqrstuvwxyz0123456789; var nonce ; for (var i 0; i len; i) { nonce chars.charAt(Math.floor(Math.random() * chars.length)); } return nonce; } console.log(JSON.stringify(genNonce.toString()));把控制台输出的长字符串复制到全局变量里比手动转义靠谱得多。第二是代码管理。全局变量里存着一大段代码字符串没有语法高亮没有版本控制多人协作时你会分不清这套变量值是哪一次的版本。Eval 方案适合临时顶一顶或者跨 Collection 少量复用不建议作为长期唯一的公共函数方案。3. 方案二Collection 级 Pre-request Script公共函数的官方主场3.1 配置步骤与代码组织方式如果你只在一个 Collection 内部共享公共函数我会毫不犹豫推荐 Collection 级 Pre-request Script。这个方案不需要把代码字符串化不需要eval代码编辑器里有完整的语法高亮和自动补全改起来舒服得多。操作非常简单在 Collection 上右键选 Edit进入 Pre-request Script 标签页把公共函数的代码直接写进去。例如var ApiUtils { genTimestamp: function() { return Math.floor(Date.now() / 1000).toString(); }, genNonce: function(len) { var chars abcdefghijklmnopqrstuvwxyz0123456789; var nonce ; for (var i 0; i len; i) { nonce chars.charAt(Math.floor(Math.random() * chars.length)); } return nonce; } };保存之后该 Collection 下所有请求的 Pre-request Script 里都可以直接写ApiUtils.genTimestamp()不需要再引入任何变量也不需要eval。用这套方案时我还有个习惯集合脚本只放函数定义不放具体的业务逻辑。业务逻辑永远留在请求脚本里请求脚本只负责取参数、调函数、塞进请求头。这样公共函数和具体用例就彻底解耦了以后换签名算法只需要改集合脚本一个地方。3.2 作用域细节为什么集合脚本里的函数在请求脚本里能用这是我在实际使用中确认过的一个重点。Collection 级 Pre-request Script 执行时确实是在当前沙箱的全局作用域里跑由于脚本按照集合级 - 文件夹级 - 请求级的层级顺序执行集合级脚本里通过function声明或var声明的对象在请求级脚本里是可以直接访问的。但这里有一个非常容易踩的坑const和let声明不会挂载到全局对象上。我见过同事把集合脚本写成const ApiUtils { ... };然后在请求脚本里调用ApiUtils.xxx()在部分 Postman 版本里会稳定报错在另一些版本里时好时坏非常磨人。所以我在集合脚本里统一用var 对象名 {}配合函数属性的方式从根源上避开作用域不确定性。还有一点函数内部访问pm对象时也要注意时机。比如你写了一个getBaseUrl()里面返回pm.environment.get(baseUrl)。这个函数在集合脚本定义时只是被声明真正执行是请求发出前一刻。因此函数体内访问环境变量完全没有问题它会读取到最新切换过来的环境。反过来如果你在集合脚本顶层直接写var baseUrl pm.environment.get(baseUrl);这行代码集合脚本一执行就取值快照了之后再切换环境、改变量baseUrl这个值都不会刷新。公共函数里请避免定义时取值尽量让函数在调用时才去读取变量。3.3 公共函数在 Tests 阶段也能复用很多人以为集合级 Pre-request Script 只在请求前有效其实它的作用域贯穿整个沙箱生命周期。请求发送结束之后Tests 脚本执行时同样可以访问到集合脚本里定义的函数。这意味着你可以把公共断言也放进集合脚本里而不是在每个请求的 Tests 里复制一遍。我通常会在集合脚本里放一个assertSuccess风格的公共方法ApiUtils.assertSuccess function(res) { var json res.json(); if (json.code ! 0) { console.log(业务返回码非0 JSON.stringify(json)); throw new Error(业务异常code json.code msg json.msg); } return json; };请求的 Tests 脚本里就只写一行var body ApiUtils.assertSuccess(pm.response); pm.test(返回数据包含list字段, function() { pm.expect(body.data).to.have.property(list); });这样一来公共的返回码校验逻辑只维护一份断言的可读性也高了不少。3.4 这套方案的边界Collection 级脚本也不是万能的。它的作用域严格被限制在同一个 Collection 内如果你有多个 Collection 需要共享同一套公共函数就得在每个 Collection 里各贴一份。对这种跨 Collection 的场景我会结合全局变量方案把是否稳定、是否跨集合作为选型标准。另外集合脚本里的代码如果过于庞大每次请求都要解析执行一遍虽然真实影响不大但确实没必要把几百行重型工具代码全部塞进去。4. 方案三require() 内置库公共函数也能模块化4.1 Postman 沙箱自带的能力比你想得多很多新手不知道Postman 脚本沙箱其实是支持 CommonJS 风格的require()的只不过它不能像 Node.js 那样加载你本地的任意文件只能加载沙箱预置的库。我在日常项目里最常用的几个内置库包括库名用途示例crypto-js各种哈希、HMAC、AES 加解密moment时间格式化、日期计算lodash数组、对象、字符串处理cheerio在响应 HTML 中解析元素postman-collection操作 Collection 结构数据使用方式非常直接var CryptoJS require(crypto-js); var moment require(moment); var time moment().format(YYYY-MM-DD HH:mm:ss); var sign CryptoJS.HmacSHA256(data, secret).toString();这条思路和公共函数的结合点在哪里在于你的公共函数不一定都是自己手写的纯函数很多签名、加密、解析逻辑需要依赖成熟库。不要自己去实现一个 MD5直接在集合公共脚本里require就好。4.2 用 require() 编写公共函数的完整形态以签名场景为例。我们可以在 Collection 级 Pre-request Script 里写var CryptoJS require(crypto-js); var AuthUtils { genTimestamp: function() { return Math.floor(Date.now() / 1000).toString(); }, genNonce: function(len) { return CryptoJS.lib.WordArray.random(len / 2).toString(); }, buildSign: function(appId, timestamp, nonce, secret) { var raw appId timestamp nonce secret; return CryptoJS.SHA256(raw).toString(); }, attachAuth: function(request) { var appId pm.environment.get(appId); var secret pm.environment.get(secret); var timestamp this.genTimestamp(); var nonce this.genNonce(16); var sign this.buildSign(appId, timestamp, nonce, secret); var headers request.headers; headers.add({ key: X-App-Id, value: appId }); headers.add({ key: X-Timestamp, value: timestamp }); headers.add({ key: X-Nonce, value: nonce }); headers.add({ key: X-Sign, value: sign }); } };请求脚本里只需要AuthUtils.attachAuth(pm.request);require的库在集合公共脚本里加载一次整个集合所有请求都能用。这比每个请求里重复写CryptoJS.HmacSHA256(...)要干净太多。4.3 require 方案的两个认识误区第一个误区是Postman 能直接 require 我自己写的本地文件。至少到目前Postman 脚本沙箱不能像 Node.js 那样require一个相对路径的 js 文件它只能加载内置库和通过配置引入的外部库。所以如果你希望团队共享一套自定义公共模块要么用前面讲的变量字符串方案要么把公共代码放进集合脚本要么借助新版本支持的外部库配置把第三方依赖 URL 引进来。自定义的那段逻辑终究还是需要通过集合脚本或全局变量来承载。第二个误区是require 模块和 eval 函数不能共存。事实上我经常在集合脚本里先写好公共函数再在公共函数内部用 require 加载依赖两个方案完全不冲突。公共函数的骨架由集合脚本提供依赖能力由 require 解决。这也是我最推荐的组合方式。5. 实战把签名、鉴权、断言统一成一套公共函数5.1 业务需求拆解为了把前面的方案串起来我用一个实际场景走一遍完整流程。假设公司接口要求每个请求的 Header 里带四个参数appId、timestamp、nonce、sign签名规则是SHA256(appId timestamp nonce secret)。secret 不直接暴露在 Header 中只在服务端和测试配置里存在。这个需求如果不做公共函数每个请求的 Pre-request Script 都要写一遍时间戳生成、随机数生成、字符串拼接、SHA256 签名、Header 设置大约二十多行重复代码。有了公共函数之后所有请求统一收敛为一行调用。5.2 完整实现集合脚本 请求脚本我按方案二加方案三的组合来做先在 Collection 的 Pre-request Script 里写入完整工具对象var CryptoJS require(crypto-js); var AuthUtils { genTimestamp: function() { return Math.floor(Date.now() / 1000).toString(); }, genNonce: function(len) { return CryptoJS.lib.WordArray.random(len ? len / 2 : 8).toString(); }, buildSign: function(appId, timestamp, nonce) { var secret pm.environment.get(secret); return CryptoJS.SHA256(appId timestamp nonce secret).toString(); }, attachAuth: function() { var req pm.request; var appId pm.environment.get(appId); var timestamp this.genTimestamp(); var nonce this.genNonce(16); var sign this.buildSign(appId, timestamp, nonce); var headers req.headers; headers.add({ key: X-App-Id, value: appId }); headers.add({ key: X-Timestamp, value: timestamp }); headers.add({ key: X-Nonce, value: nonce }); headers.add({ key: X-Sign, value: sign }); }, assertSuccess: function() { var json pm.response.json(); if (json.code ! 0) { throw new Error(业务异常 JSON.stringify(json)); } return json; } };请求的 Pre-request Script 写AuthUtils.attachAuth();请求的 Tests 写var body AuthUtils.assertSuccess(); pm.test(list 字段存在, function() { pm.expect(body.data).to.have.property(list); });这里有一个细节需要提醒attachAuth内部我用了pm.request。在较新的 Postman 沙箱里这没有问题如果你用的是某些老版本pm.request可能取不到完整的 header 操作接口这时可以改用request全局变量效果是一样的。执行后打开控制台查看请求头确认四个X-开头的 Header 都已经加上即可。5.3 为什么这套设计比每个请求各写各的强从表面看公共函数只是省了复制粘贴。但更深层的好处是当后端的签名算法从 SHA256 升级到 HMAC-SHA256或者要求增加一个盐值你只需要改AuthUtils.buildSign这一个函数所有请求在下一次执行时自动生效不需要再去逐个检查哪几个请求漏改了。我后来多次体会过这个优势接口协议一变更整个集合的维护成本几乎为零。公共函数还可以继续扩展。比如把genNonce改成更安全的随机源、把签名规则改成先排序再拼接、把assertSuccess扩展出特定的错误码处理逻辑都是在公共脚本里一个函数的事。接口测试集合的稳定性和可维护性往往就是这么一点点撑起来的。6. 几个我实测翻过车的细节6.1 全局变量更新了但公共函数还是旧的用全局变量存公共代码时最容易出这种问题你更新了 Globals 里的utils字符串代码看起来也保存了但某个请求执行出来还是老逻辑。原因通常是你在另一个环境里做的修改却没有把utils变量同步到当前环境。全局变量跨环境同步有一定滞后性在多人协作的 Workspace 里更是如此。后来我把公共函数的最终版本放到了 Collection 级脚本里全局变量只存跨 Collection 的通用字符串哪个集合没同步删掉重新拉取一次就好。6.2 字符串转义和换行地狱用 eval 方案时最大的敌人是转义。函数体一旦稍长引号、换行、正则表达式混在一起很难在变量输入框里保持原样。上面提到过用JSON.stringify(fn.toString())来生成字符串但这里还要提醒一个补充不要试图把整个对象字面量直接JSON.stringify后丢进变量因为JSON.stringify会丢弃函数属性。你要处理的是独立的函数或者把函数转换成可执行的源码段再拼接成整体。如果是团队里经常更新公共代码我会建议直接放弃手动编辑全局变量字符串这个动作改成在某个专门的请求脚本里维护源码执行一次后自动写入全局变量。这样至少代码有语法高亮改起来不容易出错。6.3 公共函数不要吞掉环境变量的最新值我在 3.2 提到过定义时取值和调用时取值的区别这里再强调一次。公共函数内部凡是涉及环境变量的都应该写成函数体内的取值逻辑// 错误示范 var appId pm.environment.get(appId); function buildSign() { return appId ... ; } // 正确示范 function buildSign() { var appId pm.environment.get(appId); return appId ...; }Postman 的环境变量是可以在多个环境之间切换的。如果公共函数在定义阶段就把 appId 取出来存成了快照那么你从 dev 环境切到 uat 环境函数仍然在用 dev 的那份值签名就会一直通不过。这类问题非常隐蔽因为在本地一次跑通时根本发现不了。6.4 公共脚本别写成一个巨型仓库最后一条经验公共函数的抽象层级要控制好。把签名、时间戳、随机数这类高频稳定的逻辑抽出来是值得的但如果有人把几十个接口各自特有的业务分支也塞进集合脚本这个集合脚本就会变成谁都看不懂、谁都不敢改的垃圾堆。我一般只把工具类和断言类逻辑放公共层业务分支判断留在请求脚本里。公共代码行数控制在两百行以内超过这个量级说明应该考虑拆分 Collection 或者把复杂逻辑挪到接口层之后的辅助服务里。我在实际项目里现在的标准做法是Collection 内共享的函数全部放在 Collection 级 Pre-request Script用一个var 命名空间对象暴露内部用require加载 crypto-js 等依赖只有真正需要跨 Collection 复用、且变更频率很低的纯函数才会塞进全局变量配合 eval 使用。这套组合已经在多套接口测试集合里稳定跑了一年多期间经历过三四次签名规则调整每次都是改一个函数完事。建议你也早点把这些重复代码收拢起来省下的时间足够多做不少真正有价值的测试设计。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表