Postman Pre-request Script:接口测试中请求参数的预处理与调试
用Postman做接口测试最常被问到的不是“怎么发送请求”而是“请求参数能不能在发出去之前先做点自定义处理”。很多时候参数不是写死的它可能是带时间戳的签名可能是上一个接口返回的token也可能是不同环境下的字段映射。直接在请求面板里改参数改一次两次还行遇到签名、关联、批量执行就完全顶不住。Postman其实给了一个很关键的入口Pre-request Script也就是在请求发送之前先跑一段JavaScript脚本。你可以在脚本里读取当前请求对象、改变量、重新计算请求体甚至临时发一个辅助请求去拿某个值然后再带着处理后的结果发送主请求。这篇文章不打算照着官方文档念而是从我实际测试中踩过的坑出发讲讲请求参数预处理到底怎么设计、怎么写、怎么调试。1. 先理解发送顺序Pre-request Script到底在什么时候跑1.1 一次请求的三个阶段很多人在Pre-request Script里写了半天发现变量没生效或者改的body根本没发出去第一反应是“脚本是不是不支持”。其实不是脚本不支持而是没搞明白脚本在什么阶段执行。Postman一次请求的生命周期简单分三步Pre-request Script阶段请求还没发出去脚本先运行。这个阶段适合生成/修改参数。发送阶段Postman真正构造HTTP请求把URL、Header、Body里的{{变量}}替换成实际值然后发出去。Tests阶段响应回来之后执行适合从响应里提取参数并保存。所以关键结论是**Pre-request Script先跑Postman再做变量替换和发送。**你在脚本里用pm.variables.set或者pm.environment.set写入的内容在请求真正发送时会被用来替换对应的占位符。你在脚本里直接用pm.request对象做的修改也会带着发送出去。如果你还是分不清可以把Pre-request Script当成“请求发出前的最后一关”所有参数在这里过一遍手再放行。1.2 用Console验证脚本到底有没有跑调试脚本生效最简单的方式是打开Postman的Console面板快捷键CtrlAltC或CmdAltC在脚本里打几条console.log再看右侧请求预览。例如我在Pre-request Script里写console.log(脚本开始当前环境变量, pm.environment.get(env)); pm.variables.set(myParam, hello-postman); console.log(写入局部变量, pm.variables.get(myParam));点击发送之后Console面板里会依次看到脚本开始当前环境变量...和写入局部变量...然后在请求预览里能看到myParamhello-postman被替换进实际请求。如果Console里没有看到打印说明脚本压根没执行或者有语法错误如果看到了但请求里没有变化那问题多半出在“变量作用域”或“参数位置”上。阶段能做什么不能做什么Pre-request Script生成变量、改请求对象、临时发送辅助请求不能读取当前请求的响应还没返回请求发送变量替换、编码、发送不能临时改变参数Tests解析响应、断言、保存变量不能改变已经发出的请求2. 要处理的参数本质上逃不出这三类2.1 动态值时间戳、随机数和防重放最常见的需求是动态值。接口要一个不重复的订单号、要一个当前时间戳、要一段随机字符串这些都不能写死在请求面板里。Postman里有一种内置动态变量比如{{$timestamp}}、{{$randomInt}}、{{$guid}}用起来确实方便。但内置变量的局限也很大它只能给你一个“当前时间戳”或“随机整数”没法做偏移量计算没法格式化成人人可读的ISO时间更没法把多个随机值组合成一个有业务含义的字符串。比如我要生成“当前时间往前推10分钟”的时间内置变量做不到就得写脚本pm.variables.set(tenMinutesAgo, new Date(Date.now() - 10 * 60 * 1000).toISOString());然后请求体里写startTime: {{tenMinutesAgo}}这样每次发送都会动态计算。这才是真正意义上的自定义。2.2 关联值上一个接口的结果要带过来接口之间有依赖关系是接口测试里绕不开的场景。登录接口返回一个token后面的业务接口都要带着创建订单接口返回一个订单号下一个查询接口要用。这种参数不是你在面板上能预先填出来的只能从上游接口的响应里取。一般在登录接口的Tests脚本里把token存到环境变量const json pm.response.json(); pm.environment.set(token, json.data.token);然后在业务请求的Params或Headers里写{{token}}。但如果业务请求的参数结构比较复杂比如token要放在Body里某个嵌套字段甚至要经过一段处理后才能用那就需要在Pre-request Script里读取并二次加工。2.3 签名与格式改造加密、字段映射和条件填充还有一类是数据格式改造。比如服务端要求参数按字典序拼接后用HMAC-SHA256签名客户端可能要求userName映射成username或者某个字段在测试环境不传、生产环境必须传这些都属于“请求发出前自定义处理”的范畴。这类需求共同点很明显它们依赖“当前请求”的上下文而不是固定写死的一组变量。所以接下来我们讨论的两种处理方式分别对应“简单参数写入”和“复杂请求修改”。方式适用场景缺点内置动态变量快速取当前时间戳、随机数不能做组合、偏移、格式化Pre-request Script 变量替换依赖关联参数、环境变量、数据驱动遇到复杂嵌套body要改对象本身Pre-request Script pm.request直接修改需要读取当前请求内容再改写对脚本能力要求更高一点3. 基于变量的参数处理作用域、关联和数据驱动3.1 六个变量作用域我一般这么选Postman的变量作用域有多个层级很多人在这里栽跟头。简化一下你主要关注这五种局部变量Local脚本里用pm.variables.set写入请求结束时自动消失适合一次性的临时值。数据变量DataCollection Runner导入CSV/JSON时产生的当前行数据用pm.iterationData.get(字段名)读取只存在当前迭代。环境变量Environment按环境隔离用pm.environment.get/set操作适合token、baseURL这种要跨请求共享的内容。集合变量Collection整个集合共享用pm.collectionVariables.get/set操作适合环境无关的公共配置。全局变量Global所有集合都能读用pm.globals.get/set操作我通常只放非常通用的东西。变量优先级从高到低大概是局部变量 数据变量 环境变量 集合变量 全局变量。也就是说如果环境变量里有一个token但你在Pre-request Script里又用pm.variables.set(token, ...)设了一个局部token请求里的{{token}}最终会替换成局部变量。我的建议是临时计算值用局部变量跨请求共享值用环境变量环境无关的固定配置用集合变量。不要把所有东西都丢给全局变量全局变量用一多追查问题会非常痛苦。3.2 从上游响应提取参数再交给下一个请求这是接口自动化里最典型的用法。先看一个登录后取token的完整链路。登录请求的Tests脚本里写const resp pm.response.json(); pm.environment.set(accessToken, resp.data.token); pm.environment.set(userId, resp.data.userId);然后下一个业务请求的Pre-request Script里从环境变量里取出来再做本地处理const token pm.environment.get(accessToken); if (!token) { throw new Error(token不存在请先执行登录接口); } const body JSON.parse(pm.request.body.raw); body.auth { token: token, channel: pm.environment.get(channel) || h5 }; pm.request.body.raw JSON.stringify(body);这里有个常见的顺序误区有人会在同一个请求的Pre-request Script里试图读取“上一个阶段”设置的变量但Pre-request Script永远跑在发送之前而Tests脚本是发送之后跑的。如果是同一个请求你要的变量还没被写进去如果是两个请求执行顺序上必须先跑完第一个请求的Tests才会跑到第二个请求的Pre-request。Collection Runner里尤其要注意。3.3 数据驱动场景下的参数预处理用Collection Runner跑批量数据时CSV或JSON里的每个字段会变成数据变量你可以直接在Pre-request Script里读取也可以结合变量做变换。比如我有一份测试数据里面有个phone字段但每条数据要求手机号不能重复我可以在Pre-request Script里这样处理const basePhone pm.iterationData.get(phone); const suffix String(Date.now()).slice(-4); const uniquePhone basePhone.slice(0, 7) suffix; pm.variables.set(uniquePhone, uniquePhone);然后在请求体里把手机号字段写成{{uniquePhone}}跑多少条数据都不会冲突。这种数据驱动的预处理思路比你在CSV里手工维护一堆唯一值要省事得多。4. 直接修改pm.request四种常见的“改包”方式4.1 为什么要绕过变量直接改包变量替换适合“定义好占位符再去填充”的场景。但有些接口测试参数是动态拼出来的请求面板里可能已经手动填了一个对象你想在发送前把它整体改掉或者你不想让请求面板里出现一堆{{变量}}。这种情况更适合直接操作pm.request对象。pm.request就是当前请求本身它在Pre-request Script阶段可以被读、被改。你可以改URL、加Header、重写Body所有修改在请求真正发送时生效。这个能力比单纯设置变量要强得多。4.2 修改URL和query参数如果我想给所有请求加一个timestamp参数直接操作query列表pm.request.url.query.add({ key: timestamp, value: String(Date.now()) });如果想修改已有的page参数pm.request.url.query.each((param) { if (param.key page) { param.value 1; } });这里注意pm.request.url.query是一个PropertyList对它的修改是实时生效的。each可以遍历每个参数add可以新增参数如果参数已经存在想覆盖可以先遍历找到后设置value或者直接删掉再添加。4.3 修改raw JSON请求体这是最常用的场景。先解析pm.request.body.raw字符串为对象改完再序列化回去try { const body JSON.parse(pm.request.body.raw); body.userId pm.environment.get(userId); body.requestId Math.random().toString(36).slice(2); pm.request.body.raw JSON.stringify(body); pm.request.body.options { raw: { language: json } }; } catch (e) { console.error(请求体不是合法JSON, e); }为什么要加pm.request.body.options因为把raw字符串写回之后Postman需要知道它以什么格式展示和发送如果不更新options在某些版本里会出现“发送了但服务端看不懂”或“Console预览显示的不是JSON”的情况。4.4 修改表单、Headers和组合用法如果是x-www-form-urlencoded表单pm.request.body.urlencoded.add({ key: nonce, value: Math.random().toString(36).slice(2) });如果是form-datapm.request.body.formdata.add({ key: file, value: temp.txt, type: text });修改Header同样可以直接操作const hasSign pm.request.headers.some(h h.key.toLowerCase() x-sign); if (!hasSign) { pm.request.headers.add({ key: X-Sign, value: signValue }); }一个完整组合场景是先读取当前请求体从环境变量里取token再写回body最后加一个Headerconst token pm.environment.get(accessToken); const body JSON.parse(pm.request.body.raw); body.token token; pm.request.body.raw JSON.stringify(body); pm.request.body.options { raw: { language: json } }; pm.request.headers.add({ key: X-Client-Channel, value: api-doc-test });这套组合基本上能覆盖我遇到过的绝大多数“改包”需求。5. 六个能直接抄的预处理场景脚本5.1 统一加签名HMAC-SHA256开放接口通常要做签名防篡改我在集合级Pre-request Script里放一段通用逻辑所有请求都会自动带签名const secret pm.environment.get(appSecret); const timestamp Math.floor(Date.now() / 1000).toString(); const nonce Math.random().toString(36).slice(2, 10); const params { appKey: pm.environment.get(appKey), timestamp, nonce }; const signStr Object.keys(params) .sort() .map(key ${key}${params[key]}) .join(); const sign CryptoJS.HmacSHA256(signStr, secret).toString(CryptoJS.enc.Hex); pm.request.headers.add({ key: X-AppKey, value: pm.environment.get(appKey) }); pm.request.headers.add({ key: X-Timestamp, value: timestamp }); pm.request.headers.add({ key: X-Nonce, value: nonce }); pm.request.headers.add({ key: X-Sign, value: sign });这里的思路是每个请求发送前都会重新跑一遍脚本所以每次的时间戳和nonce都不同签名自然不同。关键是服务端要求“按key字典序拼接”这个排序逻辑必须和你的后端约定完全一致否则签名永远验证失败。5.2 登录token自动注入请求体有些项目的token不是放在Header里而是要藏在请求体的某个字段里。用变量替换麻烦用脚本就比较直接const token pm.environment.get(accessToken); if (!token) { throw new Error(没有accessToken请先登录); } const body JSON.parse(pm.request.body.raw); body.profile body.profile || {}; body.profile.userToken token; body.profile.userToken Bearer token; pm.request.body.raw JSON.stringify(body); pm.request.body.options { raw: { language: json } };这样做的好处是不管请求面板里的body长什么样脚本都负责在发送前把token塞进去防止有人手工测试时漏填。5.3 字段名映射与空值清理前后端字段不一致又不想改接口文档那就只能在测试侧做兼容。我在Pre-request Script里做过一次字段映射const body JSON.parse(pm.request.body.raw); const fieldMap { userName: username, mobileNumber: mobile, orderId: order_id }; Object.keys(fieldMap).forEach(oldField { if (body[oldField] ! undefined) { body[fieldMap[oldField]] body[oldField]; delete body[oldField]; } }); Object.keys(body).forEach(key { if (body[key] || body[key] null) { delete body[key]; } }); pm.request.body.raw JSON.stringify(body); pm.request.body.options { raw: { language: json } };这个脚本比较适合“同一个字段查不到原因”的场景把空字符串和null清掉很多服务端参数校验问题一下就暴露了。5.4 用pm.sendRequest先换取临时凭证有些接口要求先调用一个身份服务拿临时凭证主请求再带着这个凭证访问。Pre-request Script里支持pm.sendRequest并且会等回调执行完再发主请求。pm.sendRequest({ url: pm.environment.get(authServer) /oauth/token, method: POST, header: Content-Type: application/json, body: { mode: raw, raw: JSON.stringify({ appId: pm.environment.get(appId), appSecret: pm.environment.get(appSecret) }) } }, function (err, res) { if (err) { throw new Error(获取临时凭证失败 err.message); } const json res.json(); pm.environment.set(tempToken, json.accessToken); pm.request.headers.add({ key: Authorization, value: Bearer json.accessToken }); });实际使用时要确认如果临时凭证有有效期主请求并发量一大可能多个请求同时去换令牌建议在脚本里先判断环境变量中是否已有未过期的token避免每次都重复请求。5.5 批量测试时为每次迭代生成唯一手机号跑批量注册接口时最烦的就是数据重复。我在CSV里只放一个手机号前缀脚本里补足后几位const prefix pm.iterationData.get(phonePrefix) || 139; const timestampPart String(Date.now()).slice(-6); const randomPart Math.floor(Math.random() * 900 100); const phone prefix timestampPart randomPart; pm.variables.set(uniquePhone, phone);请求体里写mobile: {{uniquePhone}}这样每次迭代都是不同的手机号。这个思路同样适用于生成订单号、邮箱、用户名等唯一值。5.6 环境切换时覆盖Host或baseURL多环境跑测试时我的习惯是不直接改请求面板里的URL而是在Pre-request Script里根据环境变量统一覆盖const env pm.environment.get(env); const hosts { dev: http://dev-api.internal.example.com, test: http://test-api.example.com, staging: https://staging-api.example.com }; const targetHost hosts[env] || hosts.dev; pm.request.url.host targetHost.replace(https://, ).replace(http://, ).split(:)[0]; pm.request.url.port targetHost.includes(:) ? targetHost.split(:).pop() : null;实际项目里host和port的拆法要看请求URL结构但我更推荐的做法是只更新环境变量里的baseUrl请求面板里统一写{{baseUrl}}/api/xxx。这样既直观又不需要在脚本里拆URL。6. 常见的坑和排查套路6.1 脚本没生效先分清楚执行顺序和报错如果你发现参数没有变化优先检查三件事。第一Console里有没有报错。JavaScript脚本里任何一行语法错误都会导致Pre-request Script直接中断后面的改写自然不生效。最常见的错误是漏了分号、括号不匹配、引号类型混用。第二脚本是不是被覆盖了。Postman里同一次请求可以同时存在集合级Pre-request Script和请求级Pre-request Script执行顺序是集合级先跑、请求级后跑。如果集合级脚本里设了一个变量请求级脚本里又设了同名变量后者会覆盖前者。排查时用console.log输出看每一步的结果。第三是不是在错误的阶段读取变量。Tests脚本里设置的变量要等到下一个请求的Pre-request Script阶段才能读到。同一次请求的Pre-request阶段去读还没产生的变量拿到的只会是undefined。6.2 变量作用域冲突调试时要分清楚“读”和“写”调试脚本时不要只看pm.variables.get的结果。这个方法是按优先级读取局部变量、数据变量、环境变量、集合变量、全局变量里的第一个同名值。如果你想确认“环境变量里的token到底是多少”应该用pm.environment.get(token)而不是pm.variables.get(token)否则你可能读到了某条数据变量或者局部变量导致你以为环境变量被改了。反过来pm.variables.set写入的是局部变量请求结束就没了。想把值保留到下一个请求必须用pm.environment.set或pm.collectionVariables.set。我见过很多新人写了pm.variables.set下一个请求取不到误以为是Postman有bug其实只是作用域不匹配。6.3 改body失败的几种原因直接修改pm.request.body.raw后发出请求服务端却收不到更新后的字段这种情况通常有几种原因。一是body类型不是raw。如果你的请求体是x-www-form-urlencoded或form-data用raw去改就没有意义应该分别用pm.request.body.urlencoded.add或pm.request.body.formdata.add。二是改完raw字符串后没有更新pm.request.body.options。有些场景下Postman会按旧的格式解析发送比如你原本是JSON改成options: { raw: { language: json } }能确保正确。三是JSON解析失败。如果请求体里写的是非法JSONJSON.parse直接抛异常脚本中断。所以常用try/catch包一层至少能在Console里看到具体报错而不是莫名其妙地发出一个空body。6.4 签名不一致时从哪些角度找原因签名是预处理里最容易“看似正确但结果错误”的场景。我自己排查时按这个顺序来时间戳单位服务端要秒你用了毫秒或者反过来。参数顺序要求按key排序但排序规则是ASCII码大小写敏感。拼接格式a1b2还是key1value1key2value2中间是否有分隔符。编码中文参数是否做了URL编码Unicode是否和前后端一致。加密算法HmacSHA256是否把签名串先做了一次UTF-8编码。参与签名的参数范围是不是把请求体里所有字段都签了还是只签了指定参数。最简单的方法先用服务端同学给的一段参考代码在本地Python或Node里跑一条静态请求然后把签名结果和Postman脚本里输出的一次结果对比哪个位置不一致一目了然。6.5 推荐的最小调试流程我现在的习惯是三步走在Pre-request Script最后加一行console.log(JSON.stringify(pm.request.toJSON(), null, 2));打开Postman Console点击发送。在Console里看请求预览确认参数是否按预期处理。如果请求预览里还不对说明脚本修改没有生效如果预览里对了但服务端说不对那问题就在编码、签名规则或数据格式上和Postman无关。最后再分享一个小技巧根据我个人实际操作中的体会Postman里的Pre-request Script适合做轻量参数处理凡是逻辑超过几十行、需要多接口拼装、要连数据库或者读文件的我都不建议硬塞进脚本里那样调试和维护成本都会变高。我的习惯是公共逻辑放到集合级脚本特殊逻辑放在请求级脚本环境变量当作跨请求的唯一状态局部变量当作一次性临时值。这样团队里即使新人也敢改脚本不会一看到复杂代码就想重写。最后一个小技巧把Postman的Console常驻在界面上每次跑脚本时留意请求预览那一栏。所有自定义处理后的最终参数都会显示在那里多数“看起来没生效”的问题其实看一眼就有答案。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →