多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Postman接口关联与参数化实战:从token传递到数据驱动测试

Postman接口关联与参数化实战:从token传递到数据驱动测试 接口测试做多了你一定会碰上这种事登录接口返回一个token后续每个需要鉴权的接口都得把这个token放进请求头里或者创建一个新用户紧接着要拿着新用户id去更新资料、下单、删除。如果每次都是手动登录、复制token、粘贴到下一个请求接口一多、测试数据一变人就开始麻了。更别提还要用几十组账号密码去验证同一个登录接口的校验逻辑——手改一次跑一次效率低还容易漏。Postman里的接口关联和参数化解决的就是这两类问题一个是请求之间的数据依赖一个是测试数据本身的批量驱动。这篇就围绕这两件事把我实际项目里怎么配置、怎么写脚本、踩过哪些坑完整拆一遍适合正在用Postman做服务端接口测试的同学也适合前后端联调时想省点重复劳动的开发。1. 接口关联和参数化到底在解决什么问题1.1 手工作坊式接口测试的尴尬先聊痛点。接口测试和页面功能测试最大的不同在于接口之间往往存在数据依赖。页面测试时你可以在界面上一步步操作浏览器会自动把状态带着走但接口测试是“请求-响应-再请求”的离散动作每一个请求都是独立的上一个请求返回了什么不会自动送到下一个请求里去。一个典型场景用户登录接口返回{ code: 0, data: { token: abc123, userId: 88 } }然后你要查这个用户的订单列表请求头里要带Authorization: Bearer abc123请求路径或参数里要带userId88。手工做就是登录一次复制一次token粘贴到下一个请求。一次两次无所谓但当你面对几十个接口、每个都要带token去测各种业务场景时这套复制粘贴流程会占用大量时间而且很容易漏。还有一个更隐蔽的问题token是有有效期的通常一两个小时就过期了。手工复制粘贴时如果token过期了你下一个请求返回401你还得重新登录重新复制。整个过程不断被打断测试节奏非常差。参数化的痛点则是另一类测试数据的重复劳动。比如接口要对“不同账号类型”做校验管理员、普通用户、未登录用户各有不同的预期结果。手工跑就是改一个账号、跑一次、再看结果一个用例三组数据就要重复执行三次相同操作。真正做接口自动化回归的人都希望脚本只写一遍数据放在外部文件里跑的时候自动循环取用。1.2 关联与参数化的分工把这两个概念放在一起理解很多人容易混淆。我一般这样区分接口关联解决的是“接口之间动态数据的传递”参数化解决的是“同一接口在不同测试数据下的重复执行”。比如登录token传到后续请求这是关联用10组账号密码循环跑登录接口这是参数化。两者经常配合使用你先用参数化跑登录接口把每一组账号登录拿到的token都存起来再关联给后面的业务接口这就形成了一个完整的数据驱动链路。维度接口关联参数化核心问题请求A的响应值被请求B引用同一接口用多组数据执行典型场景token、订单号、商品id、状态值传递登录账号、分页参数、新增记录、状态枚举实现方式Tests脚本读取响应、写入变量、再引用环境变量、CSV文件、JSON文件常见误区以为关联就是手动复制粘贴以为参数化就是把URL里的值改成变量需要说明的是参数化不一定非要外部文件。你只是想让不同环境跑不同地址环境变量就够了但如果是多组业务数据的循环CSV或JSON文件会更高效。后面我会把这几种方式都走一遍。2. 变量体系Postman处理关联与参数化的地基2.1 环境变量、全局变量、数据变量的区别Postman做关联和参数化全是建立在变量机制上的。所以第一步不是写脚本而是搞清楚变量存在哪。Postman里有三类经常打交道的变量。第一类是全局变量Globals它在所有环境、所有请求里都能用适合放团队通用的东西比如统一的网关地址前缀。但全局变量有个问题它不分环境如果你同时维护测试环境和生产环境把生产地址放进全局变量就麻烦了切来切去容易出错。第二类是环境变量Environments它绑定在一个具体的环境里。你可以建“测试环境”“生产环境”两个环境里面都定义baseUrl、username、password这些同名变量只是值不同。切换环境时同样写法的{{baseUrl}}会自动取当前环境的值。接口关联里的token也推荐放环境变量因为它和具体环境强相关——测试环境的token不能用到生产环境上。第三类是数据变量Data Variables。它来自外部数据文件比如CSV、JSON文件只在Collection Runner或Newman跑批时存在。数据变量的优先级最高它可以在运行时覆盖同名的环境变量和全局变量。这正好是参数化批量跑数据要用到的机制。2.2 变量作用域与优先级Postman查找变量时会按照一个固定顺序数据变量Data最优先其次是环境变量Environment再其次是全局变量Global最后是内置动态变量。这个优先级在实际使用中有一个很坑的点如果你在环境变量里定义了username又在CSV文件里也定义了username那CSV里的值会覆盖环境变量里的值不管CSV里的变量是在哪个iteration。很多人跑批时发现“明明环境变量设置对了怎么跑起来用了别的值”八成就是这种同名覆盖导致的。所以我自己的习惯是数据文件里的字段名尽量带业务前缀避开和环境变量重名。比如CSV里用testUserName环境变量里用username或adminAccount避免无意识的覆盖。变量引用时如果想代码里精确读取用pm.variables.get(username)会按优先级取想强制读环境变量就用pm.environment.get(username)。2.3 动态变量不用写代码也能生成随机数据除了自己定义的变量Postman还内置了一批动态变量形式上也是{{变量名}}的样子但值由Postman运行时自动生成。最常用的几个{{$timestamp}}当前时间戳10位秒级适合生成唯一的时间参数。{{$randomInt}}0到1000的随机整数适合分页页码、随机数量。{{$guid}}随机UUID适合订单号、流水号。{{$randomEmail}}随机邮箱地址适合注册类接口。{{$randomUserName}}随机用户名。比如注册接口要求用户名唯一你不想每次手动改名字在Body里直接写成username: {{$randomUserName}}每次执行都会生成新值。动态变量在接口关联中的用法也很常见一个请求里同时想传“创建时间”参数直接用{{$timestamp}}比手动填当前时间省事得多。这里需要区分一个概念动态变量不是参数化数据文件它解决的是“随机数据生成”而不是“多组数据循环”。如果你要测10个固定账号的登录差异动态变量帮不上忙要用CSV。3. 接口关联实操从登录token到跨接口传参3.1 用Tests脚本提取响应值并写入变量接口关联的第一步是在“源头接口”里把需要传递的值提取出来存成变量。这个动作发生在响应返回之后所以脚本要写在接口的 Tests 标签页里。最核心的代码就三行var res pm.response.json(); pm.environment.set(token, res.data.token); pm.environment.set(userId, res.data.userId);第一行是把响应体解析成JSON对象第二行和第三行是把data下的token、userId字段值写入当前环境的变量。注意pm.response.json()和pm.response.text()不是一回事后者拿到的是原始字符串无法直接用点语法取字段。写完之后建议先加一行日志确认变量真的存上了console.log(token pm.environment.get(token));调试时打开Postman左下角的Console能看到每次请求的脚本输出。这一步很多人忽略以为变量没存上其实是字段路径取错了或者环境没选对。有一个细节如果在环境中提前创建了同名的token变量set操作会覆盖它的值如果环境里没有这个变量Postman也会自动创建。但从团队协作角度我建议在环境变量面板里提前声明好变量名并给一个默认空值。这样其他人拉取你的环境模板时能直观看到这个接口流程依赖哪些变量排查问题也有方向。3.2 在其他接口中用变量引用变量存好之后目标接口里直接写{{token}}就能引用。Postman会在发送请求前完成变量替换把{{token}}替换成当前环境里token变量的值。关键看你想放哪个位置放在Header里比如Authorization: Bearer {{token}}放在URL路径里比如https://api.example.com/user/{{userId}}放在Query参数里比如?page{{page}}放在Body里比如{ id: {{orderId}} }写完之后注意一个视觉提示如果变量成功渲染输入框里的变量名会变成红色字体后面显示当前的值如果变量没有被识别{{token}}会保持原样发出去。看到原样输出基本就是变量名写错或环境没选中。这里有个容易忽略的点Postman请求发送前做变量替换是发生在脚本执行之后的。更准确说请求的 Pre-request Script 会先执行再替换变量并发送请求而 Tests 是在收到响应之后执行。所以我上面提到的 token 写入必须发生在源头接口的 Tests 里而不是 Pre-request Script 里因为这个接口本身发送请求时 token 还不存在。3.3 多级联场景token、id、状态的层层传递实际项目里接口关联很少只有一步。最常见的是三层甚至四层串联登录拿token查询列表拿业务id再拿业务id去更新或删除数据。举一个实际的例子。假设有一个用户管理系统流程是登录接口返回token。查询用户列表接口返回包含多个用户的数据。用列表中第一个用户的id去修改他的昵称。再拿同一个id删除这个用户。登录接口的Tests里写var res pm.response.json(); pm.environment.set(token, res.data.token);用户列表接口的Tests里写var res pm.response.json(); var userList res.data.list; if (userList userList.length 0) { pm.environment.set(targetUserId, userList[0].id); } else { console.error(用户列表为空无法设置targetUserId); }修改用户接口的URL或Body里写PUT https://api.example.com/user/{{targetUserId}} { nickname: 新的昵称 }删除用户接口的URL里同样写DELETE https://api.example.com/user/{{targetUserId}}这套链路跑通之后你在Runner里一次执行四个接口会依次完成token传递、id提取、修改、删除。手动测试时需要复制两次数据token和id脚本里只需要维护好提取逻辑。这里的重点在于每次Runner跑批时用户列表返回的数据可能会变所以targetUserId不应该写死在环境变量面板里而是由Tests动态写入。这也是关联和静态参数化的关键区别关联的值是运行时计算的不是事先配好的。3.4 依赖顺序控制为什么请求顺序不能乱接口关联依赖执行顺序。如果源头接口还没跑后面的目标接口就拿不到变量。在Collection里跑批时Postman会按照Collection中请求的排列顺序依次执行默认从上到下。你可以在Runner界面的 Run Order 区域拖拽请求调整顺序也可以勾选只执行部分请求。比如你有30个请求这次只关心登录和查订单那在Runner里取消勾选其他请求只保留这两个顺序保持登录在前、查订单在后。但要注意如果你单独点开“查订单”这个接口直接Send而当前环境里还没有token那请求会以{{token}}原样发出——这时候后端一般会返回401或参数错误。这不是Postman的问题而是接口关联的前提条件没满足必须先跑源头接口把变量准备好。所以建议在Collection里把带关联关系的请求放在同一个文件夹命名时用“01-登录”“02-获取用户列表”这样的数字前缀既方便查看依赖关系也避免Runner里顺序混乱。4. 参数化实操数据驱动的三种落地方式4.1 用环境变量做最简单的静态参数化很多人一提到参数化就以为必须用CSV文件其实最简单的参数化就是环境变量。典型场景是切换测试环境你在测试环境里定义baseUrl https://test-api.example.com生产环境里定义baseUrl https://api.example.com请求URL写成{{baseUrl}}/api/v1/user/list。需要跑哪个环境切换右上角的环境下拉框就行请求本身不用改一个字。类似地公共请求头也可以做同样的参数化。比如多个接口都需要的X-Tenant-Id你可以放在环境变量里在请求Headers里写X-Tenant-Id: {{tenantId}}。测试环境的租户id和生产环境不一样时这套机制能省下很多重复配置。这种方式的优点是简单直观、不需要管理外部文件缺点是数据量大了之后环境变量面板会变得臃肿而且没法做“同一脚本多次运行不同数据”的循环。它更适合放静态配置而不是测试用例数据。4.2 CSV文件驱动测试数据核心方式当你要用多组数据跑同一个接口时CSV文件是最常用、最轻量的方案。以登录接口为例完整操作分三步。第一步准备CSV数据文件。用记事本或Excel编辑一个文件第一行是变量名之后每一行是一组测试数据username,password,expectCode,expectMsg zhangsan,pass123,200,登录成功 lisi,pass456,400,用户名或密码错误 admin,admin123,200,登录成功需要注意第一行变量名必须和请求里引用的名字完全一致逗号分隔不要有多余空格如果文件里有中文保存时选择UTF-8编码否则Runner读取时可能出现乱码。第二步在请求里引用变量。URL、Headers、Body、Tests里都可以用POST {{baseUrl}}/api/login Body: { username: {{username}}, password: {{password}} }这里有个注意点单个请求运行时Postman不知道CSV文件的存在所以直接点Send{{username}}会被当成未定义变量可能原样发出或显示为空。CSV变量只有在Runner选择数据文件后才会被注入。第三步在Runner里指定数据文件。打开Collection的Runner勾选登录请求在Data File区域选择刚才的CSV文件点击Run。Postman会按行循环执行第一轮跑第一行数据第二轮跑第二行数据依此类推。如果文件有3行数据且Iterations设置为3就会跑3轮如果Iterations小于行数只取前几行如果大于行数数据用完后迭代会停止多设的迭代次数不会生效。CSV参数化和断言配合起来才是完整的数据驱动。登录接口的Tests里可以这样写期望值断言pm.test(状态码断言, function() { pm.response.to.have.status(200); }); pm.test(业务码断言, function() { var res pm.response.json(); pm.expect(res.code).to.eql(parseInt(pm.variables.get(expectCode))); });注意这里读取数据文件里的expectCode用的是pm.variables.get()因为CSV字段属于数据变量而且CSV读出来的值一般是字符串所以先parseInt再比较避免类型不一致导致断言失败。4.3 JSON文件驱动多组数据CSV适合扁平的键值对数据但如果测试数据有嵌套结构比如注册接口需要传一个包含地址对象的BodyCSV表达起来就很别扭。这时候用JSON文件更合适。JSON数据文件的格式是一个数组数组里的每个对象对应一次迭代的变量集[ { username: u001, password: p123, expectCode: 200, address: { city: 北京, street: 中关村大街 } }, { username: u002, password: p456, expectCode: 400, address: { city: 上海, street: 陆家嘴 } } ]在Runner的Data File Type里选择application/json请求Body里同样用{{username}}引用。嵌套的JSON对象也可以直接用变量引用{ username: {{username}}, address: {{address}} }这里的{{address}}会被替换成完整的JSON对象字符串。不过实际用的时候要小心格式最稳妥的办法是在Tests或Pre-request Script里通过pm.variables.get(address)读取后再处理而不是直接在Body里拼JSON否则容易因为引号、逗号问题导致Body格式错误。我一般只在CSV行的字段是简单字符串时才直接在Body里写变量复杂的嵌套结构都用脚本来组装。4.4 Runner批量执行的完整步骤以上三种参数化方式最终都要通过Runner来批量执行。Runner不只是点一个Run按钮那么简单里面有几个参数值得仔细设置。Iterations迭代次数。如果选了数据文件建议和文件行数保持一致如果没选数据文件这个次数就是请求重复执行的次数。想验证某个接口在连续执行5次下是否稳定可以把它设为5。Delay请求之间的延迟单位毫秒。主要用来模拟真实用户操作节奏也避免触发服务端的频率限制。我自己做接口回归时一般设置300到500毫秒如果被测服务比较脆弱或者在做并发压力相关的冒烟测试会适当调大。Log Responses是否保存响应内容。保存响应对排查问题很有用但数据量大时会拖慢Runner速度而且日志文件会很大。我一般是问题排查阶段勾选正常回归不勾。Keep variable values是否保留本次运行时修改的变量值。勾选后跑完之后环境变量里存的是最后一次运行结束时的值不勾选变量会回滚到运行前的状态。如果你的脚本逻辑会频繁修改环境变量建议保持勾选方便下一轮调试如果只想让运行环境“干净”可以不勾。还有一个容易被忽略的点Runner里可以勾选是否忽略断言错误继续执行。在Runner设置里默认遇到失败会继续跑下一个请求但有些版本或配置下会中断。如果你希望所有数据都跑完哪怕中间有失败的记得确认没有勾选“Stop run if a test case fails”之类的选项。4.5 断言与数据文件字段的联动参数化的另一个好处是断言也可以跟着数据走。你不用为每组数据单独写断言只需要把期望值也放进数据文件然后在Tests里通过pm.variables.get()读取。比如一个创建订单接口你用CSV文件传了不同的商品类型期望返回的状态码不同。CSV里可以加一列expectStatus断言写成pm.test(创建订单返回码正确, function() { var res pm.response.json(); pm.expect(res.status).to.eql(pm.variables.get(expectStatus)); });这样每一轮迭代都用当轮数据文件里的期望值来判断断言不再写死。数据驱动真正闭环不是“脚本循环跑数据”而是“每轮数据都带着自己的预期结果脚本自动校验”。5. 常见问题与排查技巧实录5.1 变量不生效的三种典型表现变量不生效是新手最容易碰到的问题而且表现形式还不一样。第一种是{{token}}原样出现在发出的请求里说明Postman没找到这个变量的值。排查方向确认右上角环境是否选对、变量名是否和写入时完全一致、有没有多余空格。第二种是变量在URL里显示为红色但发出后服务端收到的还是占位符。这种情况多半是变量在请求发送时才被求值但你的变量是在上一个接口的Tests里写入的而当前请求的Pre-request Script把变量值又覆盖成了空值。可以在Pre-request Script里加一行console.log(pm.environment.get(token))看一下。第三种是变量明明存在但值在Runner跑批时被重置了。这通常是因为你在多个接口的Tests里都set了同一个变量而某个接口响应里这个字段为空set进去一个空值或undefined导致后续接口拿到空值。建议在set之前先判断字段是否存在比如if (res.data res.data.token) { pm.environment.set(...); }。5.2 脚本取值失败最容易踩的坑我见过最多的脚本问题是把pm.response.json()写成pm.response.text()后用JSON解析的逻辑去取值自然取不到还有人把路径写错比如响应结构是{ data: { token: xxx } }却写了res.data.data.token或res.token。排查这类问题最快的方法不是猜而是先在Console里打印完整响应var res pm.response.json(); console.log(JSON.stringify(res));看清楚层级之后再去调整取值路径。另外注意有些接口返回的JSON是数组套对象比如res.data.list[0].id如果谁都不看响应结构就写res.data.id肯定取不到。这类错误只要养成“先log再写脚本”的习惯基本能避免一大半。5.3 Token过期pre-request scripts自动刷新接口关联最烦的问题就是token过期。手工测试时过期了可以重新登录但Runner跑批跑到一半token过期后面的用例全挂。解决思路是在依赖token的接口的Pre-request Script里写一个“token快过期时自动重新登录”的逻辑。大致思路是取出当前token和过期时间如果token不存在或剩余时间少于1分钟就调用登录接口重新获取token并写入环境变量。简化的代码示例var token pm.environment.get(token); var expireAt pm.environment.get(tokenExpireAt); var current Date.now(); if (!token || !expireAt || current (parseInt(expireAt) - 60000)) { pm.sendRequest({ url: pm.environment.get(baseUrl) /api/login, method: POST, header: { Content-Type: application/json }, body: { mode: raw, raw: JSON.stringify({ username: pm.environment.get(adminUser), password: pm.environment.get(adminPass) }) } }, function(err, res) { if (!err) { var resJson res.json(); pm.environment.set(token, resJson.data.token); pm.environment.set(tokenExpireAt, current 3600 * 1000); } }); }注意登录接口本身不要放这段自动刷新逻辑否则会循环调用登录。一般来说只把它放在业务接口的Pre-request Script里业务接口发请求前判断token是否需要刷新。5.4 Runner批量执行时遇到的各种坑批量执行时常见的坑有几个。第一个是CSV文件编码导致中文乱码报错信息不明显但返回值里的中文消息全变了。解决办法用记事本或编辑器把文件另存为UTF-8编码。第二个是CSV最后一行之后如果有一个空行Runner可能会把空行也当成一组数据导致最后一批请求的参数全是空字符串。检查文件末尾确保没有多余空行。第三个是断言失败导致Runner提前中断。有些版本的Runner默认遇到测试失败会继续但如果你在请求脚本里写了抛出异常的代码比如pm.expect(...).to.eql(...)之外的throw new Error(...)可能会中断整个运行。避免在Tests里主动throw让断言自然地走pm.test。第四个是环境变量在不同iteration之间被污染。比如第一轮登录设置了一个token第二轮登录因为账号密码错误响应里没有token字段上一轮的token还留在环境变量里。这不是Postman的bug是你没有在第二轮把token更新掉。可以在登录接口Tests里不管成不成功都先把token清掉再根据响应决定是否写入。5.5 数据文件字段与请求体类型不匹配CSV读出来的值全是字符串哪怕你的CSV里写的是200Postman拿到的也是字符串200。如果你在请求Body里写count: {{count}}希望发送数字类型的200那么实际发出的是字符串还是数字取决于你在Body里怎么拼。如果你用raw JSON格式直接写count: {{count}}发送的是字符串写count: {{count}}才会被当成数字。这两种写法在服务端严格校验类型时会有完全不同的结果。建议在CSV里保存时保持原始类型意图在Tests里用parseInt、parseFloat或JSON.parse做类型转换不要依赖Postman自动处理。现象可能原因处理建议请求里输出{{token}}原文环境未选择/变量名错误检查环境下拉框和变量拼写变量存在但值为空响应字段为空时set覆盖set前先判断字段是否存在Runner第二次迭代取值不变环境变量被同名数据变量覆盖数据字段加前缀避免同名冲突中文参数乱码CSV编码不是UTF-8另存为UTF-8编码断言比较总失败数字类型不一致统一用parseInt或String()转换跑到一半token失效无自动刷新逻辑Pre-request Script里加刷新token6. 实战经验与个人习惯6.1 变量命名与项目协作规范接口测试一旦涉及团队协作变量命名不统一会带来很多隐性成本。我自己带项目时一般定几条简单的规矩环境变量用驼峰或小驼峰命名比如baseUrl、token、targetUserIdCSV数据文件里的字段名使用小写下划线并用业务前缀比如test_username、expect_code避免和环境变量重名造成覆盖。另外请求的命名尽量体现依赖关系。我的习惯是登录接口叫01 - 登录[保存token]查用户列表接口叫02 - 查询用户列表[保存targetUserId]。这样在Runner里一眼就能看出执行顺序和变量传递链路新同事接手时不用挨个点开看脚本。如果项目里多个环境需要共享同一套环境变量模板建议在环境变量面板里把所有变量先定义好默认值可以为空导出JSON文件放到项目仓库里。其他人导入这个文件就能获得一套结构完整的变量清单比每个人各自随手创建变量规范得多。6.2 合理划分静态配置与业务数据我踩过的一个大坑是把业务测试数据全部塞进环境变量。比如把10组账号密码定义成account1、account2...account10然后在请求里通过{{account1}}之类的引用。这种做法的问题是环境变量面板逻辑混乱、数据没法循环、维护成本极高。后来我把这类数据全部挪到CSV文件里环境变量只保留地址、端口、公共账号、token这类“环境配置”清爽很多跑数据也灵活了。一句话总结我的原则环境配置放环境变量接口依赖放动态变量批量测试数据放数据文件随机数据用内置动态变量生成。四类数据各管各的不混用。6.3 从手工测试到自动化脚本的平滑过渡很多人觉得Postman的脚本是“自动化测试”才需要学的手工测试用不上。这个想法其实不对。手工测试时接口关联能帮你省掉大量复制粘贴参数化能帮你快速验证多组数据下的响应差异动态变量能让你不必每次手动改唯一性字段。我的建议是不要想着一步到位写一套完整的自动化脚本而是先从手测场景开始遇到需要复制粘贴的地方就用变量写死遇到重复跑相同接口的情况就建一个CSV文件跑一次Runner。用到哪个功能学哪个功能等这些操作熟练之后你会发现你的Collection已经自带了一套轻量自动化能力。到时候再深入学习Newman、CI/CD集成就是水到渠成的事。最后分享一个我自己挺受用的习惯每次跑批之后不要只看Runner界面上的通过率我会特意打开Console扫一眼有没有console.error或WARN级别的输出。很多接口关联的问题在常规断言里不会直接表现为失败但Console里的报错信息能提前暴露出隐患。接口测试这行耐心和细节比技巧更重要变量机制掌握牢固之后后面用任何工具都很快。
返回列表