☰
Bruno API 脚本执行机制:变量作用域、断言与链式调用
2026/11/2 17:10:29 网站建设 项目流程

调 API 这件事,写第一个请求只要三分钟,把一百个请求维护成一套能跑、能验、能回归的流程,往往要花掉三个月。Bruno 这类离线优先的 API 客户端之所以被越来越多团队捡起来用,很大一部分原因不在界面好不好看,而在于它的脚本执行机制足够透明——一个集合就是一个文件夹,脚本就是文件里的一段纯文本,能进 Git、能被 review、能在流水线里跑。这篇就专门聊 Bruno 脚本执行机制:脚本在什么时机触发、变量在哪一层生效、请求前脚本能改什么、响应后脚本能读什么、多个请求怎么串成一条链,以及我在实际项目里踩过的那些坑。刚接触的人可以照着抄,已经在用的人可以对照着查漏。文中涉及的 API 我会标注版本差异,因为不同版本之间确实有增删,看到与你本地不一致的地方,以你所用版本的官方文档为准。

1. 先把执行模型吃透:脚本到底在什么时机跑

1.1 三个层级、三个阶段的完整顺序

Bruno 的脚本不是一个孤立的钩子,它是分层的。集合(collection)有集合级脚本,文件夹(folder)有文件夹级脚本,单个请求也有自己的脚本。一次请求从点下发送到结果落屏,脚本一共会被触发三次:请求发出前、响应回来后、断言阶段。这三层乘三个阶段,构成了一个二维矩阵,理解这个矩阵,后面所有的"为什么我的脚本没生效"都能自己推出来。

我实测下来的执行顺序是这样的:集合级 pre-request → 文件夹级 pre-request → 请求级 pre-request → 真正发出 HTTP 请求 → 请求级 post-response → 文件夹级 post-response → 集合级 post-response → 请求级 tests。注意前三个阶段是"由外向内"层层收窄,后三个阶段正好反过来"由内向外"层层放大。这个设计的道理不难想:请求前脚本是准备上下文的,必须先把最通用的东西(比如全局的时间戳、追踪 ID)铺好,再让里层脚本去补充和覆盖;响应后脚本是收割结果的,必须先让最贴近请求的那层处理完原始响应,外层才有干净的中间结果可用。

注意:如果你在集合级和请求级同时写了同名变量赋值,最终生效的一定是请求级,因为它最后执行。这不是优先级规则,而是"后写覆盖先写"的时间顺序结果,两者本质上是一回事,但理解成时间顺序更不容易糊涂。

1.2 为什么要把脚本切成"前、后、断言"三段

很多人第一反应是把所有逻辑塞进一个地方,反正都是 JavaScript。我早期也这么干过,后来在维护一个两百多个请求的集合时被结结实实地教育了一次。三段式不是为了好看,它对应的是三种完全不同的心智模型。

请求前的脚本,职责是"改变即将发出的东西"——改 URL、加 header、算签名、塞时间戳、从上一个请求的结果里取参数。它的输入是请求对象本身,输出是对请求对象的修改,没有网络参与,纯粹是本地加工。响应后的脚本,职责是"把结果沉淀下来"——提取 token、记录 ID、把响应体里某个字段存进变量供后续请求使用。它不能改变已经发生的事,只能影响后面的事。而 tests 是纯粹的判定:对就是对,错就是错,输出一个通过率。

这个切分带来的最大好处是可诊断性。线上跑挂了,你看一眼是 pre-request 报错、post-response 报错,还是 tests 断言失败,就能直接定位到是"参数没准备好"、"结果没解析出来"还是"接口确实返回错了"。三合一写法下,这三种情况都会变成一句难以定位的红色报错。

阶段能否修改请求能否读取响应典型职责报错后的表现
pre-request能不能签名、动态参数、上下文注入请求根本不会发出
post-response不能能提取变量、写日志、串联下一步请求已发出,结果可能已落库
tests不能能断言、通过率统计请求成功但判定失败

上面这张表最后一行特别值得留意。post-response里报错是"事后"报错,接口可能已经把数据写进数据库了,你以为失败了就重试,结果产生脏数据。所以我的习惯是:所有可能抛异常的逻辑(特别是 JSON 解析、数组下标取值)都必须加防御,宁可变量没设上,也别让脚本在中途炸掉。这个习惯救过我两次。

1.3 一个请求文件长什么样

Bruno 把请求存成.bru纯文本文件,脚本以代码块的形式嵌在里面。看懂这个文件结构,你就知道脚本存在哪儿、怎么被读取的。

meta { name: 查询订单列表 type: http seq: 2 } get { url: {{baseUrl}}/orders body: none auth: none } headers { Content-Type: application/json } script:pre-request { bru.setVar('traceId', Date.now() + '-' + Math.random().toString(36).slice(2, 10)); req.setHeader('X-Trace-Id', bru.getVar('traceId')); } script:post-response { const body = res.getBody(); bru.setEnvVar('lastOrderNo', body && body.data && body.data[0] ? body.data[0].orderNo : ''); } tests { test('状态码应为 200', function () { expect(res.getStatus()).to.equal(200); }); }

script:pre-request、script:post-response、tests三个块各自独立,互不共享局部变量。这一点是新手最容易忽略的地方:你在 pre-request 里const foo = 1,在 post-response 里是拿不到foo的,两个块是两次独立的代码执行。要跨阶段传值,只有一条路——用变量,也就是bru.setVar/bru.getVar。

2. 变量作用域:脚本到底能"记住"什么

2.1 六层作用域与优先级

脚本的价值有一半体现在变量上。Bruno 的变量分层比很多人想象的要多,从最外层到最内层大致是:全局变量、集合变量、文件夹变量、环境变量、请求变量,再加上一个运行期变量(runtime)。查找时遵循"由内向外"的规则,找到就停。

我把它整理成一张对照表,用的时候直接查比记规则快:

作用域读写方式生命周期适合放什么
runtimebru.setVar/bru.getVar当前这次运行,进程结束即失运行期临时状态、traceId、中间结果
requestbru.setRequestVar等单个请求范围内仅本请求使用的中间值
environmentbru.setEnvVar/bru.getEnvVar跟随环境文件,持久化baseUrl、密钥、账号
collectionbru.setCollectionVar等跟随集合,持久化集合内共享的业务常量
folder文件夹级脚本赋值跟随文件夹某个模块专用的配置
global全局环境配置跨集合持久化跨项目复用的公共值

优先级从高到低就是按这张表从上往下。也就是说,你在 runtime 层设了一个叫token的变量,那么即使环境变量里也有一个token,脚本读到的也是 runtime 的那个。这个规则非常好用——你可以在环境里放一个"默认 token"做兜底,登录接口跑完之后用bru.setVar把真实 token 顶到 runtime 层,后续所有请求自动用新的,环境文件一点不用改。

提示:这套机制在本地跑单个请求时很顺,但要注意 runtime 变量的存活范围只在"一次运行"内。单独点某个请求发送,和用 Runner 跑整个集合,是两次不同的运行,前者的 runtime 变量不会带到后者。想让某个值跨运行保留,老老实实用环境变量或集合变量。

2.2 runtime 变量与持久化变量的取舍

什么时候用 runtime,什么时候用持久化,我总结了一条很土但很管用的判断标准:这个值如果被别人git clone之后看到会尴尬吗?

密钥、真实手机号、生产环境的 token,坚决不能落到集合文件里,那只能放环境变量,而且环境文件本身不进仓库(Bruno 支持把敏感值标记为 secret 类型,界面上会隐藏,这是个很实用的细节)。而像"订单号"这种每次运行都不一样、只用来串联流程的临时值,一律用 runtime 变量,跑完就扔,不留痕迹。

还有一个容易踩的点:持久化变量的赋值是"立刻写盘"还是"退出时写盘",不同版本行为略有差异。我的做法是永远不依赖它的写盘时机——需要立刻生效的,就用 runtime 变量;只有确实需要人工核对的值,才写环境变量,而且写完自己在界面上确认一眼。

2.3 变量插值的正确姿势

URL、header、body 里写{{baseUrl}}这种双花括号,Bruno 会在发送前自动替换。但在脚本里手动拼字符串时,{{}}是不会自动展开的,得显式调用插值方法:

const url = bru.interpolate('{{baseUrl}}/orders/{{lastOrderNo}}'); const resp = await bru.sendRequest({ method: 'GET', url: url });

我见过不止一个人直接在脚本里写req.setUrl('{{baseUrl}}/orders'),然后纳闷为什么请求打到了一个叫{{baseUrl}}的域名上。记住这条:界面上的插值是编辑器帮你做的,脚本里的插值要你自己做。反过来说,如果你希望某个变量值保持原样不被展开(比如签名原文里就带花括号),那也不要用 interpolate。

3. 请求前脚本实战:签名、动态参数与请求改写

3.1 场景一:动态时间戳、幂等键与链路追踪

这是最基础也最常用的一类。很多后端接口有防重放机制,要求请求头里带时间戳和一次性随机串,超过一定时间窗口直接拒。手工每次改太蠢,交给 pre-request 脚本最合适。

const ts = Date.now().toString(); const nonce = Math.random().toString(36).slice(2, 12); req.setHeader('X-Timestamp', ts); req.setHeader('X-Nonce', nonce); bru.setVar('traceId', ts + '-' + nonce);

看起来三行就完了,实际有两个坑。第一个是setHeader的覆盖语义:同名 header 如果已经存在,多数版本是覆盖而不是追加,如果你要的是追加多个值(比如多个X-Tag),得先取出再拼接。第二个是时间戳单位。有些服务端要秒,有些要毫秒,我见过因为传了毫秒而服务端按秒解析、算出几万年后时间导致签名校验失败的案例。写这个脚本之前,一定先把服务端的校验规则问清楚,别凭感觉。

链路追踪 ID 我习惯设成 runtime 变量,然后在 post-response 里打到日志里。这样出问题时,前端日志、客户端日志、服务端日志能对上同一个 ID,排查效率是数量级的提升。

3.2 场景二:HMAC 签名与请求体规范化

这是 pre-request 脚本真正的硬仗。签名算法的难点从来不是加密本身,而是"签名原文怎么拼"。服务端定的规则五花八门:有的是方法+路径+时间戳+随机串+body 的字符串拼接,有的要求参数按字典序排好再拼,有的对空 body 有特殊约定(用空字符串还是用{})。

const crypto = require('crypto'); const appSecret = bru.getEnvVar('appSecret'); if (!appSecret) { throw new Error('缺少 appSecret,请检查当前环境配置'); } const ts = Date.now().toString(); const nonce = Math.random().toString(36).slice(2, 10); const method = req.getMethod().toUpperCase(); const path = new URL(req.getUrl()).pathname; let bodyText = req.getBody(); if (typeof bodyText !== 'string') { bodyText = bodyText ? JSON.stringify(bodyText) : ''; } const raw = [method, path, ts, nonce, bodyText].join('\n'); const sign = crypto.createHmac('sha256', appSecret).update(raw, 'utf8').digest('hex'); req.setHeader('X-Timestamp', ts); req.setHeader('X-Nonce', nonce); req.setHeader('X-Sign', sign);

这段代码有四个细节值得单独说。

第一,require('crypto')能不能用,取决于你开没开开发者模式。默认的安全模式下,脚本跑在受限沙箱里,Node 内置模块和文件系统访问是被拦的。这是好事,别人分享给你的集合不会偷偷读你本机文件。但你要做签名,就得在设置里打开允许外部模块的开关,并且清楚这意味着什么。

第二,req.getBody()的返回类型不稳定。请求体是 JSON 时,不同版本可能返回对象,也可能返回字符串。所以上面代码里先判断类型再决定要不要JSON.stringify。这个防御看似啰嗦,但不写的话,签名在本地通过、在 CI 里失败,能折磨你一整天。

第三,JSON.stringify的键顺序遵循对象定义顺序。如果服务端要求的是"按键名字典序"序列化,你必须自己写一个排序后再序列化的函数,不能指望JSON.stringify的默认行为。我见过一次线上偶发验签失败,最后发现是某次重构调整了 body 字段的书写顺序。

第四,验签失败一定要抛出可读的错误。throw new Error('缺少 appSecret')比后面莫名其妙的 401 好排查一百倍。脚本里的错误信息是写给未来的自己看的,别省这几个字。

3.3 场景三:按条件改写请求

还有些需求不属于加密,纯粹是"根据环境改行为"。比如测试环境要关掉某个特性开关、灰度环境要打特定标签、本地要跳过缓存。

const envName = bru.getEnvName(); if (envName === 'staging') { req.setHeader('X-Feature-Flag', 'new-checkout'); } if (bru.getVar('forceNoCache') === 'true') { req.setHeader('Cache-Control', 'no-cache'); }

这里的关键是bru.getEnvName()这个能力。它让同一份集合在不同环境里表现不同,而不需要复制两套请求。我做多环境联调时,靠它在一个集合里同时覆盖了三种环境的差异,维护成本直接砍掉三分之二。

4. 响应后脚本与断言:把"看一眼"变成"能验收"

4.1 post-response 与 tests 到底该怎么分

这两个块都能读到响应,很多人就随便往一个里写。分界线其实很清楚:post-response 负责"产生状态",tests 负责"判断状态"。

什么算产生状态?把 token 存起来、把列表第一条的 ID 存起来、把某个计数累加、把响应耗时写进变量。这些都改变后续运行的环境,必须放在 post-response。

什么算判断状态?状态码是不是 200、字段是不是存在、类型对不对、耗时有没有超标、数组长度是否大于零。这些只输出结论,不改环境,放 tests。

我见过有人把断言写在 post-response 里,用if+throw实现。能跑,但代价是:任何一个断言失败,整段脚本中断,后面的变量提取全都不执行,本来能继续的流程也断了。而且 Runner 界面上看不到通过率统计,只有一片红。断言就该用断言的方式写。

const body = res.getBody(); if (body && body.data && body.data.length > 0) { bru.setVar('lastOrderNo', body.data[0].orderNo); bru.setVar('lastOrderId', body.data[0].id); } else { bru.setVar('lastOrderNo', ''); }

注意这里的双重防御:body && body.data && body.data.length > 0。少写一层,遇到空响应或错误响应就会抛Cannot read properties of undefined。post-response 抛错最麻烦,因为请求已经发出去了,事务可能已经提交了。

4.2 断言写法与几个必知的坑

Bruno 内置了 Chai 风格的expect,写法很自由,但我建议固定成test(描述, 函数)的形式,这样每一条断言在报告里都有独立的名字,失败了不用猜是哪一行。

test('状态码为 200', function () { expect(res.getStatus()).to.equal(200); }); test('响应时间为毫秒级且低于 800ms', function () { expect(res.getResponseTime()).to.be.a('number').and.to.be.below(800); }); test('返回列表结构正确', function () { const body = res.getBody(); expect(body).to.have.property('data'); expect(body.data).to.be.an('array'); });

几个实际坑:

状态码断言要区分"业务码"和"HTTP 码"。很多团队 HTTP 永远返回 200,错误信息在 body 的code字段里。你的断言应该两层都覆盖,否则接口返回{"code":500}而 HTTP 是 200 时,测试全绿但业务已经挂了。

res.getBody()在响应是 JSON 时会自动解析成对象,这一点很方便。但如果接口返回的是纯文本或 HTML,拿到的是字符串,你对它做.data访问就会报错。遇到混合类型的接口,先用typeof判一下。

耗时断言容易误报。本地跑 100ms,CI 机器上跑 900ms 很正常。所以要么把阈值放宽到明显不合理的程度(比如 3000ms,只用来抓真正的性能事故),要么干脆去掉。把性能断言卡在 800ms 是给自己找麻烦。

断言失败不会中断后续断言,这是好事。一个请求里的十条断言,会给你一份完整的通过列表,而不是"第一条挂了后面全不跑"。这个行为和某些测试框架不一样,值得记住。

5. 链式调用与流程编排:让多个请求串成一条线

5.1 Runner 里的流程控制

单个请求跑通不等于流程跑通。真实的业务场景往往是:登录拿 token → 查列表拿 ID → 用 ID 查详情 → 用 ID 下单 → 校验订单状态。这五步之间有强依赖,必须按顺序来。

Bruno 的 Runner 默认按请求的seq顺序执行。这个seq就是文件里meta块的那个序号,你可以在界面上拖动调整。默认顺序执行多数情况够用,但遇到"根据结果决定走哪条分支"就不行了,这时候要setNextRequest。

// 在 post-response 或 tests 中 const body = res.getBody(); if (body && body.data && body.data.status === 'NEED_VERIFY') { bru.setNextRequest('发送验证码'); } else { bru.setNextRequest('查询订单详情'); }

用它有两个必须知道的前提。第一,只在 Runner 里生效,单独发一个请求时调它没有任何作用,也不报错——这点特别容易让人怀疑人生。第二,跳转目标是请求名,不是文件路径,名字写错会静默跳过或者报找不到,建议复制粘贴而不是手打。

注意:setNextRequest在一些版本里挂在bru.runner命名空间下(形如bru.runner.setNextRequest(...))。如果你本地调bru.setNextRequest报 undefined,先查一下你那个版本的文档,两种写法都试一遍,通常一分钟就能确认。

5.2 跨请求传参与登录态维护

最典型的链路是登录态。做法很简单:登录请求的 post-response 里把 token 提取出来,存进 runtime 变量;其他请求在 header 里写Authorization: Bearer {{token}},靠插值自动带上。

// 登录请求的 post-response const body = res.getBody(); const token = (body && body.token) || res.getHeader('x-auth-token'); if (token) { bru.setVar('token', token); bru.setVar('tokenAt', Date.now()); } else { console.warn('登录响应里没找到 token,请检查响应结构'); }

这里有两个工程化的考量。

一是"什么时候刷新 token"。如果 token 有有效期,长链路跑一半过期了会集体 401。我的做法是在每个请求的 pre-request 里加一个轻量判断:取tokenAt,如果距现在超过某个阈值(比如 25 分钟),就先调一次刷新接口,用返回值覆盖token。这样整条链路能撑很久,不用中途手动干预。

二是"不要在断言里判断登录态"。有人喜欢在 tests 里断言expect(token).to.not.be.empty,一旦没有登录就整片红。更好的做法是让 pre-request 阶段就抛错,直接告诉你"没登录,先跑登录请求",比一堆断言失败清楚得多。

5.3 数据驱动跑批

Runner 支持挂一个 CSV 当数据源,每一行跑一遍整个集合,脚本里可以直接读到当前行的列。这个能力被严重低估了,很多人还在手工改参数跑十遍。

用法上有个细节:CSV 的列名会成为变量,优先级和普通 runtime 变量在同一层附近,所以如果你的脚本里也setVar了同名变量,可能互相盖掉。我的习惯是 CSV 列名统一加前缀(比如csv_userId),脚本里读bru.getVar('csv_userId'),彻底避免冲突。另外 CSV 里读出来的值全是字符串,"0"和"false"都是真值,做布尔判断前记得自己转换。

6. 执行环境、异步与外部依赖

6.1 异步:什么时候必须 await

Bruno 的桌面版脚本运行在 Node 环境里,支持 async/await。请求前和响应后的脚本块本身可以被写成异步函数(直接写await通常也能工作,因为执行器会等待返回的 Promise)。但有一点必须清楚:只有真正返回 Promise 的操作才需要 await,比如发一个子请求、读文件、调用异步库。

// 较新版本提供的 sendRequest,具体签名以本地文档为准 const resp = await bru.sendRequest({ method: 'POST', url: bru.interpolate('{{baseUrl}}/auth/refresh'), headers: { 'Content-Type': 'application/json' }, body: { refreshToken: bru.getVar('refreshToken') } }); const data = resp.getBody(); if (data && data.token) { bru.setVar('token', data.token); }

如果你手上的版本没有sendRequest,替代方案是把刷新逻辑做成一个独立请求文件,用bru.runRequest去触发,或者在 pre-request 里跳过刷新、改用 Runner 顺序编排。我倾向后者,因为它更透明——一眼能看出流程顺序,而不是藏在一段脚本里。

提示:脚本里发子请求会让执行时间变长,而客户端通常有超时限制。链路里塞太多同步子请求,很容易撞到超时。超过两层的嵌套,建议拆成独立请求用 Runner 串。

6.2 外部依赖与安全模式的取舍

需要用到加密、日期处理、mock 数据生成这些能力时,纯手写太累。Bruno 支持在集合目录下放一个package.json,装好依赖后在脚本里require。这个能力很强,但有两个前提:允许外部模块的开关要打开,以及依赖要在集合目录里真实存在(不是全局装的)。

这条路上我踩过的坑:本地装好了依赖,把集合推给同事,他拉下来一跑就报Cannot find module。原因是node_modules被 gitignore 了。解决办法要么在 README 里写清楚"拉下来先执行一次安装",要么干脆别用外部依赖,把需要的几行逻辑内联进脚本。我现在偏向后者,二十行的哈希实现自己写一遍,比维护一套依赖省心得多。

安全模式这件事,我的建议是:日常调试保持默认的受限模式,只有在确实需要读写本地文件、加载加密库时,才临时打开开发者模式,用完关掉。别人给你的集合,第一次跑之前先扫一眼脚本里有没有读写文件的调用,这个动作花不了三十秒,但能省掉很多麻烦。

7. 常见问题与排查实录

7.1 "脚本没生效"的五种真实原因

这个现象我遇到过太多次,每次原因都不一样,整理成一张速查表,基本能覆盖九成情况。

现象最可能的原因排查动作
pre-request 改了 header 但请求里没有请求本身在 headers 块里也写了同名项,覆盖了脚本设置删掉静态 header,全部交给脚本管
变量读出来是空字符串变量名拼错,或赋值发生在读取之后在脚本里打印bru.getVar('名字')确认
单独发送正常,Runner 里报错Runner 是全新的运行时,runtime 变量没带过去检查依赖的变量是否来自上一次运行
报错require is not defined安全模式未开启到设置里确认外部模块权限
setNextRequest 毫无反应不在 Runner 中执行用 Runner 跑集合验证

第一种情况最隐蔽。请求编辑器的 headers 区域和脚本设置 header,是两个不同的来源,最终合并时谁覆盖谁,不同版本的实现可能不一样。我吃过一次亏之后,定了个规矩:凡是脚本会动态设置的 header,一律不在静态区域重复声明。一个 header 只允许有一个来源,这条规矩消灭了整整一类 bug。

第二种也很常见,尤其是变量名里的下划线和连字符。order_id和order-id在肉眼看来几乎一样,读出来就是空。养成习惯:赋值之后立刻在同一段脚本里打一次日志,确认写进去了再往下走。

7.2 报错速查与调试手法

Bruno 的脚本报错信息有时候比较简略,只告诉你哪一行炸了,不给上下文。我的调试三板斧:

第一,用console.log而不是靠猜。脚本里的 console 输出会出现在界面的控制台面板里,把关键变量的值、拼接后的签名原文、最终 URL 打出来,八成的问题一眼就能看见。签名类问题尤其如此——把服务端算的和你算的原文逐字符对比,多半是某个换行符或者空格的事。

第二,把复杂脚本拆成小段验证。签名逻辑有十行,别一次写完再跑,先写三行验证能取到参数,再写三行验证拼接正确,最后加哈希。分步验证看着慢,实际比对着一个红色的 401 排查快得多。

第三,善用断言代替肉眼。响应体有二十个字段,你不可能每次都看完。写三条断言把关键字段卡住,跑起来绿色就是对的,红色就去看具体哪条挂了。这个习惯把"检查结果"这件事从"看"变成了"验",长期收益极大。

还有一个隐蔽问题:脚本执行顺序导致的状态污染。集合级 pre-request 里如果设了一个"当前用户"变量,而某个请求的 post-response 把它改掉了,那么后续所有请求都会用被改过的值。这类问题的特征是"单独跑没问题,连着跑就出错"。排查方法是临时在每个阶段打印变量快照,对比哪一步发生了变化。

8. 工程化落地:从能用走向可维护

8.1 目录结构与脚本分层设计

当集合超过五十个请求,脚本的放置位置就变成架构问题了。我的分层原则是:能放外层的,绝不放到内层。

集合级脚本只放两类东西:全局的身份认证头注入、以及统一的追踪 ID 生成。文件夹级脚本放模块专属的前置条件,比如"订单模块需要先拿到店铺 ID"。请求级脚本只放这个请求独有的东西。

这样分层之后,新增一个请求的成本极低——复制一个同类请求,改 URL 和断言,脚本一行不用写。反过来,如果所有逻辑都堆在请求级,新增请求就是一场复制粘贴的灾难,而且改一处规则要改五十个文件。

文件夹的划分我建议按业务域而不是按 HTTP 方法。按方法分(一个 GET 文件夹、一个 POST 文件夹)看着整齐,实际上毫无内聚性;按业务分(用户、订单、支付),文件夹级脚本才有意义,因为同一个业务域的请求往往共享同样的前置条件。

8.2 脚本的评审与回归

纯文本的好处在这里体现得淋漓尽致。你可以在代码评审里像看普通代码一样看脚本 diff,能看出谁在什么时候往 pre-request 里加了一行console.log,也能看出谁把超时阈值从 3000 改成了 300。

我给自己团队定的几条规矩,供参考:脚本里不允许出现硬编码的密钥和真实账号,一律走环境变量;不允许console.log打印完整响应体,只打印关键字段,避免日志里泄露敏感信息;所有setVar的变量名统一用驼峰并在 README 里登记,防止两个人用同一个名字互相覆盖;post-response 里所有取值操作必须带防御性判断,不允许裸下标访问。

回归这块,我的做法是把核心链路的 Runner 跑一遍当作发版前的固定动作。断言不必写得特别细,把状态码、关键业务码、核心字段存在性卡住就够了。这套东西的价值不在发现新 bug,而在于防止"改 A 接口把 B 接口搞挂"这种低级事故。跑一次几分钟,比出问题后回滚排查划算太多。

最后分享一个我一直在用的小技巧:给集合写一个专门的"健康检查"请求,放在最前面,pre-request 里检查关键环境变量是否齐全(baseUrl、appSecret、测试账号),缺哪个就抛一个写明缺哪个的错误。这样任何人拉下集合第一次跑,看到的不是一堆看不懂的 401,而是一句"当前环境缺少 appSecret",上手成本立刻就降下来了。踩过几次坑之后我越来越觉得,脚本写得好不好,衡量标准不是它多聪明,而是别人接手时能不能一眼看懂、一次跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询