Postman接口测试:用Pre-request Script实现请求参数自定义处理
使用Postman如何在接口测试前将请求的参数进行自定义处理做接口测试的朋友应该都有过这种体验明明在Postman里把接口地址、请求头、请求体都填得好好的一点Send服务端返回报错。排查一圈发现根本不是接口写错了而是参数压根没达到服务端的预期。比如登录接口需要一个动态时间戳比如下单接口的签名必须用secretKey对参数做加密再比如某些接口要求先登录拿到token再带着token去请求。这些场景下参数不能是写死的静态值必须在请求发出前做一层自定义处理。Postman的Pre-request Script功能就是干这个的我平时80%的接口测试工作都围绕这个前置脚本来展开。这篇文章从实际使用角度把请求参数的动态化处理、自定义拼接、加密签名、关联依赖这些玩法完整拆解一遍。适合刚上手Postman、还在手动改参数的同学也适合已经会用变量但没深入折腾过脚本的测试工程师。看完你至少能自己写出可复用的预请求脚本把接口测试从“手动填参”升级成“自动造参”。1. 参数自定义处理的核心思路与适用场景1.1 为什么不能直接在请求里写死参数先说个最常见的例子。很多系统的接口都要求携带时间戳比如timestamp1699000000这个值每次请求都应该是当前的Unix秒级时间。如果直接在请求体里写死一个时间戳第一次Send可能没问题但第二次Send大概率就失败了因为服务端会校验时间偏差超过一定范围直接拒绝。再比如签名验证。支付类接口通常要求把所有业务参数按字典序排列拼上密钥算一个MD5或SHA256服务端用同样的算法校验。参数一旦写死签名就是固定的一旦参数有任何变动就要手动重新算一遍签名这在调试时极其痛苦。还有一类是接口关联。典型的场景是登录后返回一个token后续所有接口都要在请求头里带上这个token。如果每次测试都先跑到登录接口复制token再到下一个接口粘贴效率极低而且容易复制错。Postman的预请求脚本可以在请求发出前自动完成这些工作动态生成参数、自动计算签名、自动从环境变量读token。1.2 预请求脚本的执行逻辑Postman的脚本体系分两块Pre-request Script请求前脚本和Tests请求后脚本。Pre-request Script在请求发送之前执行Tests在请求返回之后执行。这个执行顺序是理解一切自定义处理的基础。Postman请求生命周期 编写请求参数含{{变量}}占位符 → 执行Pre-request Script → 处理变量、动态生成参数、计算签名 → 发送真实HTTP请求 → 收到响应 → 执行Tests脚本 → 进行断言、提取数据、保存变量我在第一次用的时候有个误区以为Pre-request Script只能操作变量的值不能影响请求头里的内容。后来才明白Pre-request Script本质上运行在一个JavaScript沙箱环境里它可以通过pm.*这个全局API去读取和修改Postman内部的很多东西包括变量、请求头、请求体。脚本里设置的变量会被请求中的{{变量名}}正常替换。也就是说你完全可以在脚本里把参数值算好存到变量里然后请求体里写{{timestamp}}Postman会自动填充。1.3 核心变量体系临时变量、环境变量、集合变量、全局变量参数自定义处理离不开变量Postman一共提供了四个层级的变量存储变量类型作用域优先级典型用途局部变量Local当前请求内最高pm.variables.set()仅本次请求有效环境变量Environment当前选中的环境中区分测试环境、生产环境存baseUrl、账号密码全局变量Global整个工作空间低存token、公共的密钥等跨环境数据集合变量Collection当前集合内所有请求中低存集合级别公用的参数、登录状态日常处理请求参数时我优先用环境变量和全局变量局部变量则用在临时中转的场景。比如预请求脚本里先把参数算好放进局部变量请求体中引用Tests里拿到响应后把动态的东西token、id存进环境变量让后续请求使用。这一套变量体系配合预请求脚本基本能覆盖接口测试中绝大部分的参数自定义需求。下面从最简单的写法开始逐步深入到签名的计算、数据的关联、文件的驱动。2. 预请求脚本的基础操作与常用代码模板2.1 设置动态参数时间戳、随机数、UUID最基础也最常用的一类自定义处理是把时间戳、随机数、UUID这类值在请求发出前动态生成。我直接给出我一直在用的代码模板放到Pre-request Script里就能跑。// 生成秒级时间戳 pm.variables.set(timestamp, Math.floor(Date.now() / 1000)); // 生成毫秒级时间戳 pm.variables.set(timestamp_ms, Date.now()); // 生成指定格式的时间字符串常用于订单编号、业务流水号 const now new Date(); const pad (n) n.toString().padStart(2, 0); const dateStr ${now.getFullYear()}${pad(now.getMonth() 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}; pm.variables.set(datetime_str, dateStr); // 生成范围内的随机整数比如取100000到999999之间 pm.variables.set(random_number, Math.floor(Math.random() * 900000) 100000); // 生成UUID适合作为请求唯一标识 pm.variables.set(request_id, pm.variables.replaceIn({{$guid}}));注意这里有个细节Postman内置了{{$timestamp}}、{{$randomInt}}、{{$guid}}这类动态变量可以直接在请求参数里用不写代码就能实现动态值。但动态变量有个局限——只能生成一个值不能在同一个请求里引用了两次还要保持一致也不能基于这个值做进一步运算比如把时间戳再做MD5。所以一旦需要“生成后继续处理”就得上pre-request脚本。我在实际项目里最常用的组合是时间戳 随机数拼接成订单号再把订单号和时间戳一起参与签名计算。这种“先造参、再签名”的链路单纯靠内置动态变量是完成不了的pre-request脚本是唯一解。2.2 从登录响应中提取token并自动填充接口关联是另一个高频痛点。我的标准做法分两步登录接口的Tests里写提取逻辑后续接口的请求头里引用变量。这一步做了之后整个集合的接口测试可以连贯跑通不需要人工介入。登录接口的Tests脚本// 假设登录接口返回的JSON结构是 {data: {token: xxx, userId: 123}} const response pm.response.json(); if (pm.response.code 200 response.data response.data.token) { pm.environment.set(token, response.data.token); pm.environment.set(userId, response.data.userId); console.log(Token已保存:, response.data.token); } else { console.error(登录失败未能提取token); }后续接口的请求头里写Authorization: Bearer {{token}}这样设置的逻辑很简单先手动跑一次登录接口token自动存进环境变量之后所有引用{{token}}的请求都会自动使用最新的值。token过期了再跑一次登录接口刷新即可。如果整个集合要一键跑可以把登录接口放在集合的第一个位置通过集合运行器串联下来每个请求都会先执行自己的pre-request脚本读取当前环境变量里的token。有个容易踩的坑多个接口同时引用{{token}}但是环境变量里这个值还是空的。这通常是因为登录接口还没执行成功或者提取脚本写错了字段名。调试方法是在登录接口的Tests里加一行console.log(response)在Postman底部的控制台查看实际返回结构再对着真实结构写提取代码。2.3 JSON请求体参数的动态构造如果请求体是JSON格式除了用变量占位符还有一种方式是在pre-request脚本里直接构造整个请求体字符串。这种方式更灵活适合参数数量多、需要条件判断的场景。const requestBody { username: test_user, timestamp: Math.floor(Date.now() / 1000), deviceId: pm.variables.replaceIn({{$guid}}), // 根据环境判断使用不同的参数 channel: pm.environment.get(env) prod ? app_store : dev_test, // 嵌套结构也能处理 extra: { from: pm.variables.get(source) || postman, note: dynamic body } }; pm.variables.set(request_body, JSON.stringify(requestBody));请求体这边直接写{{request_body}}即可。这里有个关键问题Postman中请求体如果是raw JSON类型那么{{变量}}会被替换成变量的值。由于我这里是先把整个JSON对象转成了字符串变量替换后就相当于把整段JSON塞进请求体。这种方式适合那些参数结构经常变化的接口改脚本比在UI上点半天要高效得多。需要提醒一点如果请求体里有中文字符或者特殊符号JSON.stringify默认不会转义中文直接放进请求体通常没问题。但如果服务端对编码敏感建议在构造字符串时用encodeURIComponent处理业务参数的值或者确保Postman请求头里带了Content-Type: application/json; charsetutf-8。2.4 处理请求头中的自定义字段参数自定义处理不只是请求体请求头同样需要。比如某些接口要求请求头携带X-Request-Id、X-Signature、X-Timestamp这类自定义字段值往往是动态生成的。处理方式跟请求体类似在pre-request脚本里算好值请求头里写变量即可。// 请求头自定义字段的动态生成 const reqId pm.variables.replaceIn({{$guid}}); const ts Math.floor(Date.now() / 1000); pm.variables.set(x_request_id, reqId); pm.variables.set(x_timestamp, ts); // 有些服务端要求请求头的sign是“请求体原文的md5摘要” // 如果请求体是一个固定字符串这里可以这样算 const bodyRaw param1value1param2value2; const CryptoJS require(crypto-js); const sign CryptoJS.MD5(bodyRaw).toString(); pm.variables.set(x_sign, sign);这里用到了require(crypto-js)这是Postman沙箱内置的加密库下一节详细讲。请求头里对应写上X-Request-Id: {{x_request_id}} X-Timestamp: {{x_timestamp}} X-Signature: {{x_sign}}3. 进阶用法加密签名、外部数据驱动与复杂场景3.1 用CryptoJS计算MD5、SHA256签名接口签名是自定义处理里最刚需的场景。很多服务端要求客户端把业务参数按照某种规则排序加上密钥后计算摘要然后把摘要放在请求里。Postman内置了crypto-js库在pre-request脚本里可以直接使用不需要额外安装任何东西。先讲最简单的MD5签名// 引入加密库 const CryptoJS require(crypto-js); // 假设业务参数是 key1value1key2value2 const paramsStr nametesttimestamp1699000000; const secretKey your_secret_key; // MD5签名把参数串加上密钥后计算 const sign CryptoJS.MD5(paramsStr secretKey).toString(); pm.variables.set(sign, sign);再讲SHA256带密钥的签名类似HMAC:const CryptoJS require(crypto-js); const paramsStr nametesttimestamp1699000000; const secretKey your_secret_key; // HMAC-SHA256 const sign CryptoJS.HmacSHA256(paramsStr, secretKey).toString(); pm.variables.set(sign, sign);但是实际项目里的签名逻辑往往比这个复杂。最常见的签名规则是把请求体中所有参数排除sign本身按参数名ASCII字典序排序用keyvalue格式拼接再用连接最后拼上密钥做摘要。我封装了一个通用的签名函数// 通用签名函数对对象参数做字典序排序后拼接并计算MD5 function generateSign(params, secretKey) { const CryptoJS require(crypto-js); // 1. 过滤掉值为空和sign字段 const filtered {}; Object.keys(params).forEach(key { if (params[key] ! params[key] ! null params[key] ! undefined key ! sign) { filtered[key] params[key]; } }); // 2. 按ASCII码排序 const keys Object.keys(filtered).sort(); // 3. 拼接 keyvaluekeyvalue const str keys.map(key ${key}${filtered[key]}).join(); // 4. 在末尾拼接密钥 const rawStr str key secretKey; // 5. 计算MD5 return CryptoJS.MD5(rawStr).toString().toUpperCase(); } // 使用示例动态构造参数并签名 const timestamp Math.floor(Date.now() / 1000).toString(); const params { appId: wx123456, timestamp: timestamp, nonceStr: pm.variables.replaceIn({{$guid}}), body: 测试订单, // 如果接口还需要其他业务参数在这里继续添加 }; const sign generateSign(params, your_secret_key); params.sign sign; // 如果请求体是JSON直接存变量 pm.variables.set(signed_params, JSON.stringify(params)); // 如果请求体是x-www-form-urlencoded则转成查询字符串 pm.variables.set(signed_params_form, Object.keys(params).map(k ${k}${encodeURIComponent(params[k])}).join());这段代码是参考了开放平台API签名规范如微信支付、支付宝后整理出的通用模板每个项目的具体规则可能有差异。我强烈建议把自己的签名函数保存成一个集合级别的预请求脚本片段通过Postman的Collections里的Pre-request Script标签页去管理这样多个接口可以共用一套签名逻辑。3.2 从CSV/JSON文件读取参数实现批量数据驱动批量跑接口测试时需要给同一接口喂多组不同参数。Postman支持在Collection Runner里导入CSV或JSON文件作为数据源配合变量占位符一次跑完所有数据组合。先看CSV文件的格式。第一行是字段名后面每行是一组测试数据username,password,expectCode test01,123456,200 test02,654321,401 test03,,400在请求体里这样引用{ username: {{username}}, password: {{password}} }然后打开Collection Runner选择集合和接口点击Data File选择这个CSV文件点击RunPostman会为每一行数据执行一次请求。数据文件里的字段会自动覆盖同名变量pre-request脚本里也能通过pm.iterationData.get(username)获取当前行的数据。JSON文件也类似格式稍有不同[ {username: test01, password: 123456, expectCode: 200}, {username: test02, password: 654321, expectCode: 401} ]批量跑的时候配合Tests脚本里的断言就能实现简单的数据驱动测试。我自己常用的断言模板const expectCode pm.iterationData.get(expectCode); pm.expect(pm.response.code).to.eql(Number(expectCode));CSV驱动最大的坑是编码问题。CSV文件如果包含中文必须保存为UTF-8编码否则Postman读取后会乱码中文参数自然也会出错。我踩过这个坑不止一次建议在Excel里编辑完数据后另存为“CSV UTF-8逗号分隔”格式。3.3 在脚本中发请求先获取验证码再调登录接口有些接口的参数依赖另一个接口的实时响应但不想手动跑两次。Postman的pm.sendRequest可以在pre-request脚本里发起一个HTTP请求等响应返回后再继续构造参数。典型场景是验证码很多登录接口需要先调“获取验证码”接口拿到验证码再拿着验证码调登录接口。// 在登录接口的pre-request脚本中先请求验证码接口 pm.sendRequest({ url: pm.environment.get(baseUrl) /api/captcha, method: POST, header: { Content-Type: application/json }, body: { mode: raw, raw: JSON.stringify({ phone: pm.environment.get(phone) }) } }, function (err, res) { if (err) { console.error(获取验证码失败:, err); return; } const data res.json(); if (data.code 0) { // 把验证码设置到环境变量登录接口的请求体使用 {{captcha}} pm.environment.set(captcha, data.data.captcha); console.log(验证码获取成功:, data.data.captcha); } else { console.error(获取验证码接口返回异常:, data); } });注意pm.sendRequest是异步的pre-request脚本会等待这个回调执行完成才开始发送正式请求所以不需要担心回调还没执行完请求就发出去了。这一点我当初理解错了以为要用什么sleep去等待其实完全不需要。pm.sendRequest的另一个用途是模拟操作链比如测试下单接口前需要先创建一个商品拿到商品ID再下单。这种场景可以在下单接口的pre-request脚本里直接调创建商品接口创建成功后把商品ID塞进下单参数。3.4 条件分支与环境切换在多环境联调的测试中同一个接口在不同环境下的参数规则可能不同。比如测试环境不需要真实支付回调但预发环境需要。pre-request脚本里可以做条件判断根据当前选中的环境动态生成不同参数。const env pm.environment.get(env); // 在环境变量里预设一个 env 字段 if (env test) { pm.variables.set(callbackUrl, http://test.example.com/callback); pm.variables.set(amount, 0.01); // 测试环境用最小金额 } else if (env staging) { pm.variables.set(callbackUrl, https://staging.example.com/callback); pm.variables.set(amount, 1.00); } else { pm.variables.set(callbackUrl, https://example.com/callback); pm.variables.set(amount, 100.00); }这种写法比维护多套环境变量再手动切换要省事因为逻辑集中在一个脚本里改动只涉及一个地方。当然如果各环境的参数差异特别大比如baseUrl、账号体系都不同那还是多套环境变量更清晰。条件分支适合“大部分相同、少部分不同”的场景。4. 常见问题与排查技巧实录4.1 脚本没生效变量名不一致太多人问“为什么我写了pre-request script请求体里的变量还是原样输出”排查顺序先确认脚本有没有语法错误看Postman控制台View → Show Postman Console再确认变量名是否一致脚本里pm.variables.set(timestamp, ...)请求体里一定得写{{timestamp}}不能写{{Timestamp}}、{{timestamp1}}最后确认变量是否被同名的静态变量干扰环境变量和全局变量里如果存在同名的、优先级更高的值会覆盖脚本里设置的值。4.2 加密结果不对注意字符串拼接细节签名测试中最多的问题出在“服务端算出来和客户端不一样”。这种情况99%是拼接细节不一致参数顺序不对、大小写不一致、包含了多余的空格、特殊字符没有编码。我总结了一个排查清单确认参与签名的参数是否跟服务端一致是否包含了空值、sign本身。确认排序规则是ASCII字典序还是其他顺序。确认拼接格式是keyvaluekeyvalue还是keyvalue连拼。确认摘要结果是否需要转大写有些接口要toUpperCase()。打印原始拼接串在控制台console.log(rawStr)跟服务端文档里的示例拼接串逐字符对比。我在控制台对比原始串的时候曾经发现是中文参数没有经过UTF-8编码导致两边不一致。后来统一在拼接前对value做encodeURIComponent问题就解决了。但注意不是所有接口都要求编码要看服务端文档的约定。4.3 批量跑数据时变量被覆盖用CSV文件驱动的时候每行数据都会覆盖同名变量这可能导致pre-request脚本里设置的值失效。比如CSV文件里有个字段叫id脚本里也设置了pm.variables.set(id, ...)按优先级迭代数据里的id会覆盖脚本里的值。解决方法是尽量避免变量名冲突。迭代数据里的字段名用有辨识度的前缀比如input_user、input_pwd脚本生成的变量用generated_id、gen_sign这类名字。或者在脚本开头用pm.iterationData.get(id)显式读取当前行数据再赋给另一个变量。4.4 请求体是form-data格式时怎么处理如果接口要求multipart/form-data格式处理方式跟raw JSON不太一样。pre-request脚本里可以设置变量但form-data里的文件类型字段无法通过变量塞一个本地文件。文本字段倒是可以。对于form-data格式我建议把可变参数的占位符直接写在对应的key的value里脚本负责生成值即可。表单字段 username {{username}} file 选择文件手动这样既保留了手动选择文件的灵活性又能动态填充文本参数。如果一定要自动上传文件目前Postman在Collection Runner里对文件字段的数据驱动支持不太好通常需要借助Newman这类命令行工具这个后面有机会再单独讲。4.5 控制台看不到日志pre-request脚本里的console.log在Postman控制台能看到但很多人不知道入口。在Postman主界面按CtrlAltCWindows或者CmdOptionCMac就能打开控制台。另外脚本报错了请求不会发送但默认情况下错误提示不太显眼控制台里能看到红色的报错信息。我平时调试脚本的习惯是分步验证先跑一个只生成变量并console.log的极简脚本确认值正确后再逐步加入业务逻辑。这样一来每次出问题都能快速定位是取值问题、计算问题还是赋值问题省去大量反复试错的时间。5. 实操案例从零搭建一个带自定义参数处理的接口测试流程前面把原理和细节拆完了这里用一个相对完整的例子把整个流程串起来。假设现在测试一个交易平台的“创建订单”接口要求如下需要登录获取token放在请求头。请求体参数包括商品ID、数量、时间戳、随机字符串。所有业务参数包括时间戳和随机字符串参与MD5签名密钥从环境变量读取。签名值放在请求体的sign字段。第一步准备环境变量。在Postman右上角打开环境管理新增一个环境配置以下变量变量名初始值当前值说明baseUrlhttps://api.example.comhttps://api.example.com接口域名token空空登录后自动填充secretKeytest_secret_2024test_secret_2024签名密钥phone1380013800013800138000登录账号第二步创建登录接口。方法POST地址{{baseUrl}}/api/login请求体是JSON{ phone: {{phone}}, password: 123456 }Tests脚本里提取tokenconst res pm.response.json(); if (res.code 0 res.data.token) { pm.environment.set(token, res.data.token); }第三步创建订单接口。方法POST地址{{baseUrl}}/api/order/create请求头设置Authorization: Bearer {{token}} Content-Type: application/json请求体先留空因为我们要在pre-request脚本里动态构造。脚本如下// 1. 生成基础参数 const timestamp Math.floor(Date.now() / 1000).toString(); const nonceStr pm.variables.replaceIn({{$guid}}).replace(/-/g, ); // 2. 业务参数 const params { productId: 1001, quantity: 2, timestamp: timestamp, nonceStr: nonceStr }; // 3. 签名逻辑与前面封装的逻辑一致 const CryptoJS require(crypto-js); const secretKey pm.environment.get(secretKey); const keys Object.keys(params).sort(); const rawStr keys.map(key ${key}${params[key]}).join() key secretKey; const sign CryptoJS.MD5(rawStr).toString().toUpperCase(); params.sign sign; // 4. 设置请求体变量 pm.variables.set(order_body, JSON.stringify(params)); console.log(构造的请求体:, JSON.stringify(params));请求体里写{{order_body}}。这样一个创建订单接口就具备了每次自动生成时间戳、随机串、自动签名、自动携带token的能力。第四步跑一遍验证。先执行登录接口token自动存入环境变量再执行创建订单接口pre-request脚本自动构造参数并签名。打开控制台能看到构造的请求体完整输出。把rawStr打印出来跟服务端文档对一下签名规则一致的话接口就能正常返回。这套流程搭好之后后续的调试只需要改业务参数签名、时间戳、token这些繁琐的事情全部交给脚本处理。接口如果签名规则变了也只需要改pre-request脚本里的那一处拼接逻辑不用每个接口去手动改参数。从我的实际使用体验来说Postman的参数自定义处理本质上就是“把手工重复劳动转成脚本自动执行”。很多人被“写脚本”三个字吓住了实际上这些脚本都是几十行的固定模式用得多了你会发现来来回回就那么几种取变量、设变量、算时间、算签名。把这些模板保存下来后面几乎不用从头写。尤其当你需要反复调试一个签名接口的时候pre-request脚本带来的效率提升是肉眼可见的——你只需要关注业务参数本身那些“每次请求都会变”的值全都交给脚本自己去生成。