首页 / 资讯中心 / 文章详情

Postman接口自动化:Tests断言校验返回结果实战指南

Postman接口自动化:Tests断言校验返回结果实战指南 ★ FEATURED ARTICLE
1. 为什么我要把接口校验交给Tests断言2019年底到2020年初那段时间前后端分离的项目越来越多接口联调和回归测试的量一下子压了上来。当时我手里维护着十几个业务接口每次后端改完代码我都要打开Postman重新点一遍请求然后盯着响应体里的JSON手动核对字段。说实话字段少的时候还好一旦返回一个几百行的嵌套对象眼睛很快就花了。更让人头疼的是人工核对常常漏掉一些“隐藏变化”——比如某个返回字段从字符串变成了数字页面解析不出来但肉眼第一眼看过去并不容易发现。后来我开始认真用Postman里的Tests面板写断言把“人工看返回结果”变成“脚本自动校验返回结果”整个流程才真正稳定下来。所谓断言可以理解成一个自动验收员请求发出去之后脚本自动检查返回结果对不对——状态码是不是200、返回的JSON里有没有关键字段、字段类型是否符合预期、业务码是否是成功值。所有检查项全部通过这条用例才算绿任何一个检查项不满足用例立刻标红并且你一眼就能看出是哪个断言挂了、具体什么值和预期不符。这篇文章主要围绕Postman里最实用的一个能力——Tests断言校验返回结果展开。内容适合正在做接口测试、想把手动回归变成自动校验的测试工程师和开发同学参考。我会从为什么需要断言、常用断言写法、接口链路的完整校验、踩坑记录以及如何接入批量执行这几个角度把能直接落地的思路和代码都整理出来。2. 高频可用的Tests断言写法2.1 最基础的断言结构pm.test在Postman的Tests面板里最核心的入口就是pm.test函数。它的写法通常长这样pm.test(响应状态码是 200, function () { pm.response.to.have.status(200); });这段代码的意思是注册一个名为“响应状态码是 200”的测试用例里面传入一个回调函数Postman在执行完请求后自动运行这个回调。回调里如果没有任何断言抛出异常测试结果就是通过如果断言失败结果就会显示为失败并且测试名称会原样显示在Results区域方便你快速定位是哪一个检查点挂了。我第一次用的时候对pm.test的理解有点偏差。我以为是“先定义一个测试函数后面手动调用”但实际不是。pm.test更像是给Postman的测试列表里添加一条测试项请求完成之后自动执行。你写了多少条pm.test测试结果里就会出现多少条结果记录。这个机制很关键因为很多人把一组断言放在一个pm.test里结果失败了却很难看出是其中哪个小条件不满足。我在实际项目中习惯将一个独立校验点对应一条pm.test宁可多写几条也不把所有条件堆在一起。2.2 校验响应状态码、响应时间和响应头状态码是最基本的返回结果校验很多接口只要状态码不是200就说明请求链路本身出了问题。但状态码通过不代表业务一定成功后文我会单独讲这个坑。先来看几种写法// 写法一直接判断响应状态码 pm.test(状态码为 200, function () { pm.response.to.have.status(200); }); // 写法二用 pm.response 对象里的 code 字段做判断 pm.test(状态码为 200写法二, function () { pm.expect(pm.response.code).to.eql(200); });两种写法都常见我一般用第一种因为阅读起来更直观。接着是响应时间接口性能也是返回结果的一部分。如果一个接口日常都要跑3秒那功能正确也不代表体验合格。这里可以设置一个阈值pm.test(响应时间低于 800ms, function () { pm.expect(pm.response.responseTime).to.be.below(800); });响应头也可以做校验比如判断返回的内容类型是否是JSON这能提前拦截一些网关返回的HTML错误页pm.test(响应头包含 JSON 类型, function () { const contentType pm.response.headers.get(Content-Type); pm.expect(contentType).to.include(application/json); });那个headers.get方法很实用取不到对应的请求头时返回null所以断言用include会比直接用equal更稳妥因为很多Web框架返回的Content-Type会带charset等参数比如“application/json;charsetutf-8”用include恰好符合实际。2.3 校验JSON返回体字段、类型和数组接口返回结果中JSON是最常见的数据格式。Postman里用pm.response.json()可以把原始响应体解析成JavaScript对象然后就能像平常操作对象一样去取字段、判断类型、比较值。先看一段完整的示例假设某个接口返回{ code: 0, message: success, data: { token: abc123, userName: tester, vipLevel: 2 } }那我们的断言可以这么写// 将响应体解析为对象 const jsonData pm.response.json(); pm.test(业务码为 0, function () { pm.expect(jsonData.code).to.eql(0); }); pm.test(返回 message 是 success, function () { pm.expect(jsonData.message).to.eql(success); }); pm.test(data 下的 token 字段值不为空, function () { pm.expect(jsonData.data.token).to.not.be.empty; }); pm.test(vipLevel 是数字类型, function () { pm.expect(jsonData.data.vipLevel).to.be.a(number); });关于to.eql和to.equal的选择是我在实际中朝大梁踩出来的细节。Chai断言库语义下to.equal使用的是严格相等对于对象和数组往往要逐个字段比对而to.eql做的是深度相等可以直接比较两个对象或数组是否在结构上完全一致。判断单个数字或字符串时两者差别不大但一旦涉及到对象整体比较我会统一用to.eql避免因为字段顺序不同或者嵌套导致莫名其妙的失败。还有几种常用的断言表达式值得记一下pm.expect(jsonData.data.list).to.be.an(array); pm.expect(jsonData.data.list).to.have.lengthOf(3); pm.expect(jsonData.data.name).to.be.a(string); pm.expect(jsonData.data.count).to.be.at.least(1);其中at.least可能受框架版本影响如果你发现依赖环境不支持直接用大于等于运算符替代也行pm.test(count 大于等于 1, function () { pm.expect(jsonData.data.count 1).to.eql(true); });2.4 字符串匹配和值的枚举范围除了精确相等返回结果校验中还经常用到“包含”和“属于指定范围”的判断。比如手机号脱敏后返回“138****8888”你想判断星号位置是否正确可以用includepm.test(手机号脱敏格式正确, function () { pm.expect(jsonData.data.mobile).to.include(****); });对于订单状态这类字段值可能在一个枚举集合内。比如订单状态有pending、paid、shipped、done四个取值那我一般会定义预期集合然后用include判断pm.test(订单状态在合法枚举范围内, function () { const allowedStatus [pending, paid, shipped, done]; pm.expect(allowedStatus).to.include(jsonData.data.orderStatus); });这种写法比直接“状态等于paid”灵活很多因为业务上有可能返回多种合法状态只要落在预期集合里就算通过。否则一旦后端从paid改成了shipped直接相等的断言就会误报反而增加排查成本。2.5 为什么用pm.test包一层而不是直接写判断语句有读者可能会问既然pm.expect本身就能判断那直接用if判断再抛出错误行不行可以但pm.test的价值在于它能把一个Assertion封装成一条独立的测试记录最终体现在测试结果面板里。如果你写一堆throw ErrorPostman只会显示脚本执行异常你很难快速看出具体哪条业务校验没通过。另外一个理解上的关键点是Postman的Tests脚本是一段请求完成后自动执行的JavaScript代码它从上到下依次运行。比如你写了三条pm.test即使第一条断言失败了第二条和第三条还是会被执行并不会因为你写在前面的断言失败就中断整个脚本。这一点很多人不知道结果就会出现“一个接口已经失败了后面的变量保存和断言还在继续跑”的情况。想中途跳过后面的检查只能在代码里自己加条件判断。3. 从登录到下单一套接口链路的完整断言编排3.1 模拟项目X的接口设计单独给一个接口写断言比较容易但实际项目中往往是多个接口相互依赖需要把上一个接口返回的数据传到下一个请求里。这里我模拟一个项目X场景是某电商后台的登录和下单流程接口名称接口路径请求方式关键参数返回结果核心字段用户登录{{baseUrl}}/api/v1/loginPOSTusername, passwordcode, data.token创建订单{{baseUrl}}/api/v1/order/createPOSTHeader: Authorizationcode, data.orderId查询订单{{baseUrl}}/api/v1/order/detail?orderId{{createdOrderId}}GET无code, data.status, data.amountText这里我用{{baseUrl}}这种环境变量写法这样切换测试环境时只需要改环境变量即可不需要改动脚本。在这个链路里真正的校验难点不单是每个接口自身的返回字段还要看接口之间数据是否正确传递。登录成功后要把token保存下来创建订单后要把orderId保存下来后面的查询接口才能正常使用。3.2 登录接口校验token并保存到环境变量先看登录接口的Tests脚本。这里的目标是校验状态码、业务码、token字段存在性并且在所有关键校验通过后把token存进环境变量const jsonData pm.response.json(); pm.test(登录接口状态码为 200, function () { pm.response.to.have.status(200); }); pm.test(登录业务码为 0, function () { pm.expect(jsonData.code).to.eql(0); }); pm.test(token 字段存在且非空字符串, function () { pm.expect(jsonData.data.token).to.be.a(string); pm.expect(jsonData.data.token).to.not.be.empty; }); pm.test(用户名与请求参数一致, function () { pm.expect(jsonData.data.userName).to.eql(pm.request.body.raw); });实际上第三个断言以我现在来看没必要写成精确比较用户名除非接口明确返回了这个字段这里只是示意。更要的是下面这段条件保存if (jsonData.code 0 jsonData.data jsonData.data.token) { pm.environment.set(token, jsonData.data.token); }之所以要加if判断是因为Tests脚本不会因为前面的断言失败而自动停止执行。如果登录接口返回的是错误码data字段下压根没有token而后面的请求又在傻傻地等token就会把错误数据一路带下去。换句话说先判空再保存是我在实际联调中总结出来的一个刚需。3.3 创建订单接口校验动态生成的orderId创建订单接口需要在Headers里带上Authorization: Bearer {{token}}然后返回一个动态生成的订单号。这个时候我们关注的不是“预期值是什么”而是“返回结果里有没有满足条件的订单号”。const jsonData pm.response.json(); pm.test(创建订单状态码为 200, function () { pm.response.to.have.status(200); }); pm.test(创建订单业务码为 0, function () { pm.expect(jsonData.code).to.eql(0); }); pm.test(orderId 为字符串且非空, function () { pm.expect(jsonData.data.orderId).to.be.a(string); pm.expect(jsonData.data.orderId).to.not.be.empty; }); pm.test(订单金额字段在合理区间, function () { const amountText jsonData.data.amountText; const amount parseFloat(amountText); pm.expect(amount).to.be.a(number); pm.expect(amount 0).to.eql(true); }); if (jsonData.data jsonData.data.orderId) { pm.environment.set(createdOrderId, jsonData.data.orderId); }这里有几个细节值得展开。第一orderId是动态生成的你没法提前知道具体值所以关键是校验它的“类型和存在性”而不是比较具体内容。第二金额这类浮点数据如果直接拿float比较很容易出问题。比如接口返回0.10JavaScript浮点运算是0.1加0.2不等于0.3的那种用to.eql(0.1)很不稳。更靠谱的做法是把金额换算成“分”再比较用整数相等来判断pm.test(金额与预期一致按分比较, function () { const returned Math.round(parseFloat(19.90) * 100); const expected Math.round(19.90 * 100); pm.expect(returned).to.eql(expected); });最早我直接比较浮点值后来被一个“多收一分钱”的bug坑过一次才养成这个习惯。接口返回结果只要是涉及金额、库存这类需要精确比较的字段统一转成最小单位再比较是最省事的方案。3.4 查询订单接口校验列表中的指定订单查询接口是我们链路里的最后一环。这里不仅校验状态码和业务码还要从返回的订单列表里找到刚才创建的那条订单再对这条订单的状态、金额做二次校验。这就涉及到一个很有价值的操作从“整个返回结果”中筛选出“目标数据”。const jsonData pm.response.json(); const expectedOrderId pm.environment.get(createdOrderId); pm.test(查询订单状态码为 200, function () { pm.response.to.have.status(200); }); pm.test(查询结果里能匹配到刚创建的订单, function () { const list jsonData.data.list; pm.expect(list).to.be.an(array); const matched list.find(function (item) { return item.orderId expectedOrderId; }); pm.expect(matched).to.not.be.undefined; }); pm.test(匹配订单的状态合法, function () { const list jsonData.data.list; const matched list.find(function (item) { return item.orderId expectedOrderId; }); const allowedStatus [pending, paid, shipped, done]; pm.expect(allowedStatus).to.include(matched.status); });从这个例子可以看出断言的本质并不复杂真正有价值的是“如何从返回结果里定位到你要校验的那部分数据”。find方法是JavaScript数组原生提供的能力比遍历取第一个元素或者写一堆for循环更简洁。这里有一个小教训断言里用到的变量如果依赖前面接口的保存结果最好在脚本开头先判空否则一旦前面链路断了这里一执行就会因为undefined而直接报错。3.5 失败快速终止的取舍在Collection Runner里跑完整套用例时如果登录接口挂了后面所有依赖token的接口肯定都会挂。这种场景下与其让后面十条用例全红不如先停掉修好登录再继续。Postman里可以通过postman.setNextRequest来控制流程比如在登录脚本里判断失败后直接跳过后续请求if (jsonData.code ! 0 || !jsonData.data.token) { postman.setNextRequest(null); }如果返回null或空字符串Runner就会停止继续执行后面的请求。但这个用法有一个限制它只在Collection Runner或Newman批量运行时有效单独Send一个请求时看不出作用。我一般在跑完整集合时加这个快速失败逻辑平时手工调试时不依赖它。关于数据保存我再强调一下环境变量和全局变量的选择。pm.environment.set是把数据写到当前环境跟随Postman右上角的环境切换变化pm.globals.set是写到全局所有环境都能访问。我的习惯是环境相关的数据用环境变量全局固定的配置才用全局变量。否则在多环境切换时很容易出现“测试环境跑的token跑到生产环境请求里”这种低级事故。4. 实测中踩过的坑断言失败排查速查4.1 还没开始断言就报错的原因断言脚本本身是JavaScript所以只要代码有语法错误、或者运行时抛了异常整段脚本都执行不了自然也就谈不上校验返回结果了。最常见的两个原因我在这里单独列出来。第一个是pm.response.json()解析异常。Postman脚本里pm.response.json()只有在响应体是合法JSON时才能正常解析。如果接口在异常情况下返回了一个HTML错误页比如网关拦截后返回一串HTML代码或者返回的JSON里包含了无法解析的转义符那这行代码一执行就会抛错。为了不崩掉后续所有断言我们需要给解析加一层保护。我常用的写法是let jsonData; try { jsonData pm.response.json(); } catch (e) { pm.test(返回结果必须是合法 JSON, function () { pm.expect.fail(响应体不是合法 JSON); }); }这样即使解析失败脚本也能继续执行到后面的断言并且明确标注出是JSON格式的问题而不是干等着看一大段红色的异常堆栈。第二个是字段取错。比如后端返回结构里data字段下面还有一层result你的脚本直接写jsonData.data.token结果拿到undefined。很多新手一看断言失败就以为接口返错了其实往往是自己路径写错了。我的建议是写复杂断言之前先在Tests面板最前面加一行console.log(JSON.stringify(jsonData))发送完请求后到Console里看一遍实际结构再动手写断言。4.2 断言逻辑写错导致“假通过”比断言失败更隐蔽的问题是“假通过”。也就是说用例显示绿色看起来全过了实际上断言本身有问题。最常见的情况是你取了一个不存在的字段然后断言它不等于某个值。比如预期期望jsonData.data.user.status 1但实际字段名是userStatus。如果直接写成以下这种就不会报错pm.test(用户状态正常, function () { pm.expect(jsonData.data.user.status).to.equals(1); });问题在于如果jsonData.data.user下面根本没有status字段那表达式的值是undefined严格等于1当然不成立测试应该是失败的。但如果你把它反过来写成“不等于一个错误值”那就很容易出现假通过pm.test(用户状态正常, function () { pm.expect(jsonData.data.user.status).to.not.eql(-1); });undefined不等于-1断言直接通过。这类问题是真真实实会出现的而且极难察觉。我最开始写断言时也这么干过后来总结出一个笨办法写完一组断言故意把一个条件改成错误值比如把期望的code改成233看测试是不是真的变红。如果改成错误值之后它还是绿的说明这条断言根本没在正常工作得赶紧检查字段路径。再比如字符串包含判断时接口返回了“success ”后面带个空格你又只写了include(success)这种通常能过不会被空格影响。但如果接口返回“Success”首字母大写而你写的是小写success就会误报失败。判断这种场景最好先把两端空格trim掉再做大小写归一的处理pm.test(返回文案包含关键字, function () { const message jsonData.message.trim().toLowerCase(); pm.expect(message).to.include(success); });4.3 常见问题速查表我整理了一张排查速查表平时遇到断言异常可以直接对照着看。现象可能原因处理方式pm.response.json() 报错返回体不是合法JSON可能被网关拦截返回HTML用try/catch包裹解析先判断Content-Type断言一直通过但明显不符合预期字段路径写错取到了undefined后做了“不等于某个值”的判断使用to.be.undefined先确认字段存在或将断言改成精确相等状态码200但断言还是失败后端把业务错误包装成了HTTP 200实际业务码非0同时断言HTTP状态码和业务code批量运行偶发失败前置请求写入的变量名与其他请求冲突检查环境变量名唯一性必要时加请求名前缀数组中某个目标元素吻合但断言找不到返回列表里的字段不是orderId而是order_id打印实际字段名做对比不要靠猜响应慢导致超时断言失败测试环境网络波动阈值放宽一点或者区分本地和CI环境的耗时指标这些场景我基本上都遇到过特别是“状态码200但业务失败”这个几乎可以说是接口测试中最高频的坑。很多后端框架统一的返回结构会在HTTP层始终保持200真正的错误通过body里的code和message表达。如果只校验状态码等于一个重要的错误信号直接被吞掉了。所以我在任何接口断言里都会要求至少同时覆盖状态码和业务码这两层。5. 把断言脚本跑进批量执行和自动回归流程5.1 用Collection Runner跑完整回归单个接口的断言写完价值是零散校验真正发挥威力的是把一组请求组织成Collection然后批量执行。Postman左上角的“Runner”入口可以打开Collection Runner选择集合、选择环境、填迭代次数就能一次性跑完所有请求和Tests脚本。在Runner的结果面板里每个请求下的每个pm.test都会对应一条独立的运行记录。比如登录接口写了4条断言这批断言全部通过才显示该请求通过只要有1条失败整个请求和对应断言就标红。这也是为什么我前面强调“一个校验点对应一条pm.test”因为结果面板就是按pm.test条目来展示的如果你的断言写得粗失败时根本看不出具体是哪一项业务条件不满足。我第一次跑完整套collection的时候最大的收获还不是“看到绿”而是“看到红”。一堆之前手工测试没发现的问题被批量暴露出来。比如某个接口在某种数据条件下会返回code20001但因为接口整体状态码还是200手工看根本不会在意断言一上它就红了。这就是把校验交给脚本的价值。5.2 用Newman把它跑进命令行和自动化流程Collection Runner适合在Postman界面里手动跑如果我们要让接口回归自动化起来就得脱离界面执行。Postman配套提供了一款命令行工具Newman它可以直接运行导出的Collection文件和环境文件并把Tests断言的结果反映在进程退出码上。我常用的命令大致是这样newman run tests/collection.json \ -e tests/env.json \ -d tests/data.csv \ --reporters cli,junit \ --reporter-junit-export reports/report.xml简单解释一下这几个参数run后面跟的是集合文件-e指定环境变量文件-d可以传入一份CSV数据文件用来做表格化驱动测试相当于用多组测试数据跑同一个请求--reporters指定要输出哪些格式的报告这里用cli控制台输出加junit报告因为junit报告可以方便地被后续的CI平台解析。第一次使用Newman时我建议先跑一条最简单的请求确认环境变量没有串环境。如果发现找不到环境变量多半是Collection里引用了{{xxx}}但e参数指定的环境文件里没有定义这个变量。如果要在自动化流水线里让“断言失败”导致构建失败关键点是理解命令行的退出码。Newman只有在所有Tests断言都通过时才返回0只要有任何一条断言失败退出码就是非0。流水线里只需要执行这个命令并检查退出状态即可。还有一个参数值得记住newman run tests/collection.json -n 3 --bail-n 3表示对整个集合跑3轮--bail表示只要出现第一次失败就让后续请求停止。这样适合快速验证“当前版本是否可部署”。5.3 断言报告怎么看跑完Newman或者Collection Runner之后我一般先看失败用例的分布。如果失败集中在一两个接口上说明大概率是这两个接口本身有变化需要打开Postman单跑确认。如果失败铺开来一大片全红那先怀疑前置公共数据断了比如登录token没取到或者环境变量被覆盖。再往细看点开一个失败请求后Tests标签页里会显示具体的断言结果。如果断言名称写得好此刻你不需要去看任何日志光凭“订单状态在合法枚举范围内”这一条就能知道是状态枚举问题还是字段缺失问题。这也是写断言时命名要尽量带业务语义的原因不要写“test1”“断言2”这类的名称真到排查阶段会非常痛苦。6. 我写Tests断言的一些个人体会6.1 先让它失败一次我写断言有一个习惯就是写完一组之后故意把期望值改错然后点Send。如果它真的红了说明脚本是在正常工作的如果它还是绿的那说明这条断言根本没有起到校验作用要么是字段取错了要么是断言逻辑本身有问题。这看起来是个很笨的验证手段但确实帮我抓出过好几次自己写错字段名导致“假通过”的问题。尤其是上面提到的那种“取到undefined然后去not.eql(-1)”的情况只有靠故意改错才能暴露。6.2 断言要跟着接口文档一起维护接口返回结果是会演变的。字段改名、类型调整、结构层级变化这些在后端重构时太常见了。一旦接口文档变了我需要跟着改的就是Tests脚本。如果断言还是老字段名那测试立刻开始报错这正是自动化回归的价值用失败提醒你接口变了。问题是如果你平时懒得更新断言时间一长断言就会和线上接口脱节最后变成“反正它一直红着大家都习惯了”。这是一个非常危险的状态。所以我给自己定了一个规矩接口文档变更时第一时间同步去改对应的断言并且跑一次完整集合确认全绿。这样这条集合才有资格作为回归防线。那些“反正它一直红着”的集合我宁愿直接把那些失效断言清掉也不要让一屋子红变成没人管的噪音。6.3 断言不是越多越好但要踩在业务关键点上刚开始写断言的时候容易陷入另一个极端就是把能判断的全写上去字段存在性、长度、类型、精确值、返回顺序……结果每次后端微调格式测试就翻红一遍维护成本很高。后来我慢慢学会做减法只对真正影响业务的关键点做断言。比如登录接口的token不能为空、业务码必须是成功、订单号生成了、列表里能查到自己创建的那条订单。这些是“业务是否真的成功”的底线。而那些“返回的data里的logo字段是不是String”之类的问题除非前端解析依赖它否则真的没必要写成一票否决式的断言。我个人在实际操作中的体会是接口自动化测试不是比谁写的断言多而是比谁能用最稳定的断言把业务流程的关键成功条件锁住。把Tests断言理解成一套自动验收机器那么验收标准需要跟业务对齐而不是跟代码实现抠字眼。再分享一个小技巧如果你发现自己经常在Postman的Console和Results面板之间反复切换可以在Tests脚本开头统一加一行console.log把关键的返回体片段打印出来这样即使断言失败你也能立刻在旁边看到“当时返回的实际值是什么”排查效率能提升一大截。对返回结果校验这件事来说让失败信息清晰、可定位有时候比多写两条断言更重要。
阅读完成 · 觉得有帮助?
咨询建站