你有没有遇到过这种情况:接口在 Postman 里一点“Send”,返回 200,绿油油一片,于是你自信地跟开发说“接口没问题”。结果接口一上线,前端页面拿不到数据,一查日志才发现,后端虽然返回 200,但响应体里塞的是一段错误信息。问题出在哪?就出在“只看状态码,不做断言”。
Postman 的断言就是这个环节的核心工具。它能在请求返回后自动检查响应数据是否符合预期,状态码、响应体、响应头、响应时间都能验,一旦结果不对就飘红告诉你“这轮测试挂了”。这篇文章我会把 Postman 里最常用的断言逐一拆开讲,同时从执行机制的角度说清楚断言到底是怎么工作的,适合刚接触接口测试、想把测试脚本写得规范一点的同学,也适合已经在用 Postman 但每次只会复制粘贴代码片段、完全不理解含义的人。
1. 断言是什么:先搞清楚它在接口测试里的位置
很多人以为“跑接口测试 = 用 Postman 发请求”。其实不对。发请求只是提交数据,验证返回结果是否符合业务预期,才是接口测试的核心动作。而这个“验证”的动作,就是断言。
1.1 没写断言的接口测试,基本等于“只测了连通性”
我见过不少团队的接口用例是这种状态:打开一个接口,填好参数,点 Send,看到 200 就截图贴到缺陷单里,标记为“通过”。说实话,这一步只能证明“服务没挂”,证明不了“接口逻辑正确”。
举个最简单的例子。你去查一个用户详情接口,传入一个不存在的用户 ID,后端处理出错时没做兜底,直接返回了 500,传统意义上的“接口通了”就不成立。但更隐蔽的情况是:后端兜底了,返回“操作失败”的 JSON,但 HTTP 状态码仍然是 200。此时你不看响应体,根本发现不了问题。更麻烦的是,这类漏测问题要等到前端联调、甚至上线后由真实用户触发,成本一下子就上去了。
断言就是干这个的。它把你对接口的所有预期——状态码是什么、返回结构长什么样、关键字段的值是什么、响应时间在什么范围内——写成一段可执行脚本。请求发完,脚本自动跑,逐条比对。全部符合,测试通过;有一条不符,立刻红牌警告。
1.2 Postman 里的断言到底写在哪个位置
打开 Postman,点开一个请求,你会看到请求地址栏下方有几个标签页:Params、Headers、Body、Pre-request Script、Tests。许多人常年只用前三个,对后面两个视而不见。
断言写在Tests标签页里。这里就是一段 JavaScript 运行环境,Postman 在收到响应后会自动执行这里面所有代码。注意和 Pre-request Script 区分,后者是在发送请求之前执行的,经常用来做签名、设置动态参数,不负责结果校验。
Tests 标签页里最基础的断言结构长这样:
pm.test("状态码是200", function () { pm.response.to.have.status(200); });pm.test接收两个参数,第一个是这条断言的名称,会直接显示在测试结果列表里;第二个是函数,函数里写具体的校验逻辑。如果函数内的断言通过,名称前显示绿色对勾,失败则显示红色叉号。就这么简单。
1.3 断言的核心四类场景
Postman 内置了大量断言方法,但日常接口测试用到的场景基本可以归成四类:
| 场景 | 典型断言对象 | 常用方法 |
|---|---|---|
| 状态合理性 | HTTP 状态码 | pm.response.to.have.status(200) |
| 内容正确性 | 响应体/JSON 字段 | pm.expect(jsonData.code).to.eql(0) |
| 链路完整性 | 响应头/ Cookie | pm.response.to.have.header("Content-Type") |
| 性能底线 | 响应时间 | pm.expect(pm.response.responseTime).to.be.below(500) |
这四类基本覆盖了 90% 以上的接口测试场景。第 2 节我逐个展开讲代码写法和容易踩的坑。
2. Postman 常用断言逐个拆解:代码、效果与误区
新手刚开始写断言,最需要的是“拿来就用”的代码,但光复制代码不够,还得知道每句脚本在干什么,遇到报错才知道怎么改。
2.1 响应状态码断言:最基础但最容易被误解
状态码断言有两种写法。第一种是 Postman 提供的快捷方法:
pm.test("状态码是200", function () { pm.response.to.have.status(200); });第二种是通用断言风格:
pm.test("状态码是200", function () { pm.expect(pm.response.code).to.eql(200); });两种写法效果一样,区别在于第一种是 Postman 内置的简化语法,专门针对“响应状态码校验”这个高频场景;第二种通过pm.expect拿到响应状态码的值再比较,更通用,适合批量判断状态码属于某一个范围的情况。
如果需要判断“2xx 范围”的状态码,可以这样写:
pm.test("状态码在2xx范围", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 202, 204]); });需要提醒的是,状态码断言只是底线。我见过很多人只做了“200 断言”就觉得测试完成了,结果响应体里返回的是一段 HTML 报错页面,这种场景在网关鉴权失效时特别常见。所以状态码断言一定要配合响应体断言一起用。
2.2 响应体字符串断言:适合粗糙校验
当一个接口返回的是纯文本、HTML 或你只关心某个关键字是否存在时,可以直接用字符串断言。
pm.test("响应体中包含期望的提示信息", function () { pm.expect(pm.response.text()).to.include("操作成功"); });这里有几个常用方法:
to.include("xxx"):包含某个字符串to.not.include("xxx"):不包含某个字符串to.equal("xxx"):整个响应体和字符串完全相等
字符串断言的优点是书写简单,缺点是容易误判。比如你想验证“用户名重复”,结果响应体里无论是“用户名重复”还是“用户名不重复”都包含“用户名”三个字,那断言就失效了。所以字符串断言只适合“粗筛”,精细校验还得看 JSON 断言。
2.3 JSON 响应体断言:接口测试的重头戏
现在大多数业务接口返回的都是 JSON,所以 JSON 断言是使用频率最高、也最容易出问题的部分。核心思路是:先把响应体解析成 JS 对象,再对对象里的字段做各种校验。
const res = pm.response.json(); pm.test("业务状态码为0", function () { pm.expect(res.code).to.eql(0); }); pm.test("message字段非空", function () { pm.expect(res.message).to.not.be.empty; });第一行const res = pm.response.json()是把 JSON 字符串解析成 JavaScript 对象的关键步骤。注意,如果响应体本身不是合法的 JSON,这一步会直接抛异常,导致后续断言全部失败。这也是新手最常见的问题来源之一。
JSON 断言还能做得更细。比如判断数组长度、判断嵌套字段、判断字段类型:
const res = pm.response.json(); pm.test("data数组长度大于0", function () { pm.expect(res.data.length).to.be.greaterThan(0); }); pm.test("userId字段是数字类型", function () { pm.expect(res.data.userId).to.be.a("number"); }); pm.test("订单状态字段值为PAID", function () { pm.expect(res.data.orderInfo.status).to.eql("PAID"); });这里用得最多的就是to.eql。它做的是“深度相等”比较,也就是说,如果你比较的是一个数组或者对象,它会逐个元素、逐个属性去比对,而不只是比对引用地址。这一点比to.equal更适合测 JSON 对象整体结构。
2.4 响应头断言:验证 Content-Type 与自定义头
响应头经常被忽略,但遇到编码问题、跨域问题、文件下载场景时,响应头就是关键证据。Postman 里可以通过pm.response.headers拿到所有响应头。
pm.test("Content-Type包含application/json", function () { pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json"); });还可以直接判断某个响应头是否存在:
pm.test("响应头包含X-Request-Id", function () { pm.response.to.have.header("X-Request-Id"); });跨域场景下经常需要判断Access-Control-Allow-Origin是否符合预期,原理一样。需要提一句,headers.get("Content-Type")获取到的值可能带charset=utf-8之类的额外信息,所以用include而不是equal更稳妥。
2.5 响应时间断言:给接口性能画一条底线
接口再慢也不能无限等。Postman 里响应时间单位是毫秒,可以直接拿到做比较。
pm.test("响应时间小于1000ms", function () { pm.expect(pm.response.responseTime).to.be.below(1000); });除了to.be.below(1000),还有to.be.above(500)可以判断“大于某个值”。响应时间断言适合做基线控制,比如核心查询接口 500ms 以内算达标。但要注意,本机调试时的响应时间和线上环境差异很大,建议先在目标环境多跑几次,拿到合理基线再写死阈值。
2.6 环境变量与全局变量断言:验证数据流转是否正确
接口测试中经常需要把上一个接口的返回值传给下一个接口,这时就要配合环境变量。断言同样可以校验变量是否已经被正确赋值。
pm.test("token已保存到环境变量", function () { const token = pm.environment.get("token"); pm.expect(token).to.not.be.undefined; pm.expect(token.length).to.be.greaterThan(0); });pm.environment.get是读取环境变量,pm.globals.get是读取全局变量,pm.variables.get则是读取当前请求级别的变量。三者的优先级和作用域不同,环境变量最常用。这个断言经常用在“上一个接口请求成功、但环境变量没有正确保存”的排查场景,能帮我们快速定位是哪一环断了。
3. 断言的工作原理:从点击 Send 到结果打勾,Postman 到底做了什么
知其然也要知其所以然。明白了 Postman 断言的运行机制,遇到脚本报错时就不会两眼一抹黑。
3.1 代码执行的先后顺序:Pre-request Script 在前,Tests 在后
Postman 中一次完整请求的执行顺序是:先说 Pre-request Script,再发送 HTTP 请求,收到响应后执行 Tests 脚本,最后把结果渲染到界面上。
这个顺序很关键。很多初学者在 Pre-request Script 里写了对上一个接口响应结果的断言,结果总是拿不到值,就是因为搞混了执行时机——Pre-request Script 跑的时候,本次请求还没发出去,更不可能有响应数据。同理,想在 Tests 里给本次请求动态加上签名参数,也是不行的,因为请求已经发出去了,加参数晚了。
3.2 神奇的 pm 对象:Postman 送给每个脚本的万能工具
你在 Tests 里写的每一段脚本,都运行在一个由 Postman 创建的特殊环境中。这个环境里预置了一个叫做pm的全局对象,它帮你把接口测试时需要用到的各种数据都集中在一个命名空间里。你可以简单理解成,pm是 Postman 送给每个脚本的一把万能瑞士军刀。
常用的几个属性和方法:
| 成员 | 作用 |
|---|---|
pm.response | 当前请求的响应对象,包含状态码、响应头、响应体 |
pm.request | 当前请求对象,可以拿到请求头和请求体 |
pm.expect | 断言函数,和 Chai 断言库的 expect 用法一致 |
pm.test | 定义一个测试用例 |
pm.environment | 环境变量操作入口 |
pm.globals | 全局变量操作入口 |
pm.variables | 当前请求级变量操作入口 |
pm.collectionVariables | 集合级变量操作入口 |
pm.response内部还有两个常用子属性:pm.response.code是 HTTP 状态码,pm.response.responseTime是响应毫秒数,pm.response.text()可以拿到原始响应字符串,pm.response.json()可以把 JSON 响应体解析成对象。
3.3 pm.test 的包装机制:如何保证多条断言互不干扰
pm.test本身是一个“包装器”。它会把传入的函数放进一个 try-catch 里执行。函数内部如果抛出了异常,比如字段不存在、类型不匹配、断言失败,Postman 会把这个异常捕获住,记为该条测试用例失败,但不会影响其他测试用例的运行。
这是 Postman 断言设计得很巧妙的一点。假设你在一个 Tests 里写了五条断言,第三条访问了不存在的属性导致报错,前两条和后两条仍然会正常执行并给出结果。这样你在排查问题时能同时看到“哪些是对的、哪些是错的”,而不是因为一条报错导致整个脚本中断。
看一下这个例子:
pm.test("断言code字段", function () { const res = pm.response.json(); pm.expect(res.code).to.eql(0); }); pm.test("断言data字段", function () { const res = pm.response.json(); pm.expect(res.data.name).to.eql("张三"); });假设接口没返回data字段,第一条断言依然会通过,第二条断言会因为res.data是undefined而报错。两条互不干扰。
3.4 pm.expect 背后的断言库:Chai 的 expect 风格
如果你接触过 Node.js 生态,对 Chai 断言库一定不陌生。Postman 内置了 Chai 的 expect 风格语法,所有的pm.expect其实就是 Chai 的expect。
链条式语法是 Chai 的特点。比如pm.expect(res.code).to.eql(0),读起来就是“期望 res.code 等于 0”。中间那个to在语义上只是为了连贯,不参与比较,真正的比较动作在最后一个方法上。你可以把它们当作一条自然语言来读。
常用链式方法整理如下:
to.eql(value):深度相等,比较值是否完全一致to.equal(value):严格相等,相当于 JavaScript 的===to.include(value):包含某个值。对字符串表示包含子串,对数组表示包含元素,对对象表示包含属性to.be.a("string"):判断类型,也可以写成to.be.a("number")、to.be.a("array")to.have.property("name"):判断对象是否有某个属性to.be.empty:判断数组、对象或字符串是否为空to.be.true/to.be.false:判断布尔值to.be.above(number)/to.be.below(number):判断大小to.be.oneOf([...]):判断是否属于数组中的某个值
明白了这些方法后,你会发现 Postman 断言写来写去就那么几个套路,真正要花心思的是“你要验证什么预期”,而不是“怎么写代码”。
4. 实测演示:两个接口场景把断言串起来用
单独讲方法容易让人觉得零散,我拿两个真实业务场景把断言串起来跑一遍,你会发现接口测试的完整链路其实很顺。
4.1 场景一:用户登录接口的断言设计
假设有一个登录接口,地址是/api/login,传参是用户名和密码,正常返回如下 JSON:
{ "code": 0, "message": "success", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", "userName": "测试用户", "expiresIn": 7200 } }在 Tests 标签页里,我会分几条断言去覆盖这个响应:
const res = pm.response.json(); pm.test("登录接口返回HTTP 200", function () { pm.response.to.have.status(200); }); pm.test("业务状态码为0", function () { pm.expect(res.code).to.eql(0); }); pm.test("响应消息为success", function () { pm.expect(res.message).to.eql("success"); }); pm.test("token字段存在且非空", function () { pm.expect(res.data).to.have.property("token"); pm.expect(res.data.token.length).to.be.greaterThan(0); }); pm.test("响应时间小于1秒", function () { pm.expect(pm.response.responseTime).to.be.below(1000); });这里有个小细节,检查 token 时先判断res.data有没有token这个属性,再判断 token 长度大于 0。两步分开是为了定位问题更精准——如果哪天接口少返回了 token 字段,报错信息会直接说是“缺少属性”,而不是“访问 undefined 的长度报错”,排查效率完全不同。
断言通过后,把 token 保存到环境变量里,供后续接口使用:
pm.environment.set("token", res.data.token);这一步通常不放在断言里,直接写在 Tests 标签页顶层即可。但要注意,它是无条件执行的,如果接口失败且没有返回 token,这里会抛异常。想稳妥一点,可以在保存前判断 token 是否存在。
4.2 场景二:查询订单接口的断言设计
第二个接口是登录后才能访问的订单列表接口/api/orders。请求头里要带Authorization: Bearer {{token}},返回如下:
{ "code": 0, "message": "success", "data": { "list": [ { "orderId": "A1001", "status": "PAID", "amount": 299.00 }, { "orderId": "A1002", "status": "UNPAID", "amount": 59.00 } ], "total": 2 } }测试脚本会这么写:
const res = pm.response.json(); pm.test("订单接口返回业务成功", function () { pm.expect(res.code).to.eql(0); }); pm.test("订单列表非空", function () { pm.expect(res.data.list.length).to.be.greaterThan(0); }); pm.test("订单总数与列表长度一致", function () { pm.expect(res.data.total).to.eql(res.data.list.length); }); pm.test("每个订单都包含orderId字段", function () { res.data.list.forEach(function (order) { pm.expect(order).to.have.property("orderId"); }); }); pm.test("已支付订单金额大于0", function () { const paidOrders = res.data.list.filter(function (order) { return order.status === "PAID"; }); paidOrders.forEach(function (order) { pm.expect(order.amount).to.be.greaterThan(0); }); });这个场景里有三个思路值得借鉴。一是“断言业务字段间的逻辑关系”,比如total和list.length是否一致,这种断言往往比单纯验某个字段更能发现深层次问题。二是“用 forEach 去遍历数组里的元素”,确保列表中的每一条数据都符合规范,而不是只检查第一条。三是“先过滤再断言”,只对满足条件的对象做校验,这样不会因为无关数据导致误报。
4.3 怎么看断言结果:Tests 面板的绿点和红点
跑完请求后,Postman 底部会有一个 Test Results 面板。每一条pm.test的命名都会出现在这里,通过时前面是绿色对勾,失败时是红色叉号,并同时显示断言库抛出的错误信息。
在 Collection Runner 批量执行时,每个接口的断言结果会汇总到最终报告中。如果一个集合里有 50 个请求、200 条断言,最终报告中会显示“200 条里通过了 196 条、失败了 4 条”,并且会标出是哪几个请求的哪几个断言出了问题。这也是为什么我强调pm.test的第一个参数必须起一个有意义的名字——在几十个接口的测试报告里,一眼找到“订单接口返回业务成功”失败,绝对比看到一个“test1”失败要快得多。
5. 高频故障排查:断言脚本飘红的底层原因与修复办法
脚本写多了难免踩坑,我把最常见的几类问题整理成了一份排查清单,遇到报错可以直接对着看。
5.1 常见断言报错与修复对照表
| 报错信息 | 原因 | 修复方式 |
|---|---|---|
There was an error when evaluating the test script | Tests 脚本语法错误,或访问了 undefined 的属性 | 检查响应体结构,先打印pm.response.json()确认字段 |
Cannot read properties of undefined (reading 'data') | 响应体里没有 data 字段,直接访问了res.data | 先做属性存在性判断,或使用可选链res?.data |
expected undefined to equal 0 | 目标字段实际值为 undefined | 确认接口返回的字段名是否写错,注意大小写 |
SyntaxError: Unexpected token | pm.response.json()解析了一个非 JSON 的响应体 | 先用pm.response.text()查看原始内容,确认是否 HTML/错误页 |
Expected 'application/json' to include 'text/html' | 响应头的 Content-Type 不符合预期 | 可能是服务端返回了错误页面,检查后端服务是否正常 |
有一个通用排查方法:在脚本最上面加一行console.log(pm.response.text()),然后打开 Postman 左下角的 Console 面板查看原始响应内容。看到真实返回后,再对照断言里的期望值,往往一眼就能找出问题。
5.2 注意 JSON 对象和数组的嵌套层级
接口返回的 JSON 结构有时候嵌套很深,常见结构是res.data.list[0].orderInfo.status。写断言时要格外注意每一层的字段名是否正确、是否多了一层或少了一层。
我的习惯是先在 Console 面板里用console.log(JSON.stringify(pm.response.json(), null, 2))打印格式化后的 JSON,然后照着打印结果一层一层写断言路径。这样能避免凭记忆写错字段,也能顺带发现后端多返回了一层包装结构之类的意外情况。
5.3 断言命名规范:让测试报告可读可用
很多人写pm.test第一个参数时很随意,什么test1、测试、TT都敢写。等集合里积累了几百条断言,跑完一轮批量测试后,光是分析报告就能耗费大量时间。断言命名建议遵守一个通用规范:动词 + 对象 + 预期结果。
推荐写法示例:
登录接口返回HTTP 200登录接口业务状态码为0登录接口返回token非空订单列表每个订单包含orderId
这种命名方法的优势在于,报告里哪怕不看具体代码,也能猜出这条断言在验证什么。多人协作时,这个习惯尤其重要。另外,一个接口下的断言尽量按“请求层面 → 业务层面 → 数据层面”的次序排列,和日常接口测试的思路保持一致。
5.4 批量执行与 CI 集成时的断言策略
Postman 的断言不只是单个请求时手动点 Send 用来“看看”,它的更大价值体现在 Collection Runner 和 Newman 命令行工具中。Collection Runner 可以将整个集合按顺序执行,并把所有断言结果自动汇总;Newman 则可以把相同的能力带到 Jenkins 等 CI 环境中,实现接口测试的自动化回归。
从个人的实用经验看,批量执行时要特别注意断言之间的“前置依赖”。比如第 1 个请求负责登录并保存 token,第 2 个请求依赖这个 token。如果第 1 个请求失败导致 token 没有保存,第 2 个请求很大概率也会失败,最终报告里会出现一长串红色。这种情况下的处理办法是:给前置请求的关键断言做好命名标记,跑完后先看前置请求的结果,再判断后续请求的失败是独立问题还是连锁反应。还能在后续请求的 Tests 脚本里对 token 是否存在做前置校验,如果 token 不存在,直接用pm.test.skip跳过该请求的用例,避免无意义的失败报告。
还有一个细节:在 Newman 集成到 CI 时,默认情况下只要有一条断言失败,命令返回的退出码就是非 0。这对流水线来说是好事,能阻断异常发版。但要注意,如果接口本身存在“偶发超时”的情况,最好在脚本里预留重试机制,或者使用 Postman 的setTimeout搭配循环请求实现简单重试,否则 CI 会因为偶发问题频繁闪红,维护成本很高。
6. 多写点、多想点:Postman 断言之外还能做些什么
接口测试做到后面,单一的请求断言只是基础能力。断言结果能不能服务到团队、能不能沉淀成可用资产,才是测试工作真正产生价值的地方。
就我个人经验来说,有两件事收益很高:一是把核心接口的断言写到足够细,做到“业务字段间的逻辑关系也覆盖到”;二是把断言命名的规范和集合的组织结构同步推广到团队里,让所有人都能在 10 分钟内看懂别人写的测试脚本。另外,在日常调试时,不要只盯着绿勾看,偶尔故意把断言值改成错误的值,确认它会红,这也是一种验证断言有效性的好办法。毕竟如果断言永远不会失败,那它跟没写也没区别。