摘要:本文分享一套已在公司内部实际运行的接口自动化测试方案------Postman + Newman + Jenkins。文章从「接口自动化到底要验证什么」出发,把测试关注点拆为协议正确性、数据契约、业务语义、流程一致性四层;随后给出可版本化的测试工程结构、Collection 组织方式与变量作用域管理,并通过登录提取 Token、创建订单传递 ID、查询订单验证业务语义、数据驱动、测试数据清理等完整链路演示落地细节。最后重点讨论 Newman 的 Node.js 执行入口、Jenkins 流水线与质量门禁、测试报告与断言设计、并发隔离、重试与超时策略,以及 Postman 与 Newman 的兼容性等工程化问题,帮助团队建立一条可追溯、可复现、可诊断的质量反馈链路。
大家好,我是 Harvey。
今天想和大家分享一套我们公司内部已经实际运行的轻量接口自动化测试方案:Postman + Newman + Jenkins。
先聊聊这套方案是怎么来的。
两三年前,我还是一名刚入行的测试新人。当时部门正处于上升阶段,我也希望借着做一些测试基建提升自己,为升职加薪多攒点筹码。接口自动化,就是那时准备推进的一件事。
调研过程中,我在网上找了不少方案,主流思路基本都是代码框架 + CSV 测试用例。当时我也顺着这个方向做了一版:用代码封装请求和断言,再把接口参数、预期结果等信息维护在 CSV 文件中。
方案做出来之后,却被 Leader 驳回了。
原因倒不是它跑不通,而是不够普适:接口一多,测试用例文件就要持续维护;业务流程一复杂,CSV 里的数据和代码逻辑之间也容易产生割裂。对于日常主要做接口调试的同事来说,新增和维护用例的门槛并不低。
而我们团队的工作风格又比较年轻、自由,我希望找到一种大家更容易接受,比较轻量,也更愿意持续使用的方式。
这时候,Postman 进入了我的视线。
当时团队里的开发和测试基本都在用 Postman 调试接口,请求参数、环境配置和接口文档本身就有一定积累。如果能在这些已有资产上补充断言和业务流程,再接入 CI,或许就不需要让大家额外维护一套完全独立的测试用例。
接下来要解决的问题就很明确了:Postman 里的 Collection 能不能脱离图形界面执行?执行结果能不能接入 Jenkins,并在测试失败时阻断流水线?
调研之后,我找到了 Newman。它是 Postman 提供的命令行 Collection 执行工具,可以运行 Collection、传入环境变量和测试数据,并生成测试报告。再由 Jenkins 负责拉取代码、注入配置、触发 Newman 和收集结果,整条链路就串起来了。
经过实践,我们最终把这套方案用到了公司内部的接口自动化中。这篇文章会围绕它的设计思路和落地过程展开,包括 Collection 如何组织、接口之间如何传递数据、凭据和测试数据怎么管理,以及 Jenkins 如何根据测试结果控制流水线。
为了方便说明,文章中的接口、字段和环境地址均为示例。实际落地时,需要替换为你们自己的服务契约和环境配置。以下展示核心实现,需根据示例契约自行创建 Collection 和测试服务。
一、先想清楚:我们到底要自动化什么?
在介绍具体实现之前,先回答一个问题:接口自动化到底要验证到什么程度?
如果只是把 Postman 里的请求按顺序执行一遍,然后检查是否返回 HTTP 200,那更像是在做接口连通性检查。要让它真正帮助我们发现问题,还需要进一步验证响应数据和业务结果。
我通常会把接口测试关注的内容分成四层:
| 层次 | 关注点 | 示例 |
|---|---|---|
| 协议正确性 | HTTP 请求与响应是否符合约定 | 状态码、Content-Type |
| 数据契约 | 响应结构和字段类型是否符合约定 | orderId 为非空字符串 |
| 业务语义 | 系统是否完成了预期业务 | 创建订单后查询到正确金额 |
| 流程一致性 | 多个接口串联后状态是否正确 | 创建、查询、取消后的状态流转 |
为什么要分这四层?来看一个例子:
json
{
"code": "ORDER_CREATE_FAILED",
"message": "库存不足",
"data": null
}
这个接口即使返回 HTTP 200,也只能说明服务返回了响应,并不能说明订单创建成功。如果测试只断言状态码,就会把一次业务失败当成成功。
因此,后面设计测试用例时,我们会在 HTTP 状态码之外,继续验证业务码、关键字段,以及多个接口之间的状态变化。
接下来,先看看这套方案的整体架构。
二、整体架构:让 Collection 成为可执行的测试资产
先看最终链路:
text
┌──────────────────────┐
│ Git 仓库 │
│ Collection / 数据 │
│ runner / Jenkinsfile │
└──────────┬───────────┘
│ checkout
▼
┌──────────────────────┐
│ Jenkins Agent │
│ npm ci │
│ 注入测试环境与凭据 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Newman │
│ 执行 Collection │
│ 输出 CLI + JUnit XML │
└──────────┬───────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌─────────────────┐ ┌───────────────────┐
│ Jenkins 测试趋势 │ │ 构建状态 / 发布门禁 │
└─────────────────┘ └───────────────────┘
三者的职责要分清:
- Postman:设计、调试和维护请求、断言与业务流程。
- Newman:在命令行或 Node.js 中执行 Collection,并将结果转化为进程退出状态和报告。
- Jenkins:准备运行环境、注入凭据、触发测试、收集报告、控制后续阶段。
一个核心原则是: Collection、测试数据、执行入口和流水线配置应该随代码一起版本化。CI 运行的测试应当能与某次提交对应起来。
Postman 工作区可以作为编辑入口,但团队要明确:到底以工作区还是 Git 中的 Collection 为执行基准。本文采用 Git 中的导出文件作为 CI 基准。
三、设计一个足够小、但完整的测试工程
建议从下面的结构开始:
text
api-tests/
├── collections/
│ └── orders.postman_collection.json
├── data/
│ └── orders.json
├── scripts/
│ └── run-newman.js
├── reports/
│ └── .gitkeep
├── .gitignore
├── Jenkinsfile
├── package.json
└── package-lock.json
各文件的职责:
| 文件 | 作用 |
|---|---|
| orders.postman_collection.json | 接口定义、脚本、断言和执行顺序 |
| orders.json | 非敏感的测试数据 |
| run-newman.js | Newman 的统一执行入口 |
| Jenkinsfile | CI 执行、凭据注入和报告收集 |
| package-lock.json | 锁定依赖树,降低环境漂移 |
.gitignore:
gitignore
node_modules/
reports/*
!reports/.gitkeep
# 本地调试时使用的敏感配置
.env
.env.*
!.env.example
初始化依赖:
bash
npm init -y
npm install --save-dev --save-exact newman
然后把生成的 package.json 和 package-lock.json 一起提交。
在 CI 中使用:
bash
npm ci
npm ci 的意义不是"安装得更快"这么简单,而是让流水线按照锁文件安装依赖。Node.js 版本也应在 Jenkins Agent 镜像或构建节点配置中固定,并验证其与项目锁定的 Newman 版本兼容。
四、先定义示例业务契约
为了让后面的代码有明确上下文,假设系统提供如下接口:
| 场景 | 方法 | 路径 | 成功条件 |
|---|---|---|---|
| 登录 | POST | /api/auth/login | 返回 access token |
| 创建订单 | POST | /api/orders | 返回订单 ID |
| 查询订单 | GET | /api/orders/{id} | 返回订单详情 |
| 取消订单 | POST | /api/orders/{id}/cancel | 订单进入取消状态 |
统一成功响应格式:
json
{
"code": "SUCCESS",
"data": {}
}
创建订单请求:
json
{
"productId": "SKU-1001",
"quantity": 2,
"clientOrderNo": "autotest-xxxxxxxx"
}
查询订单响应:
json
{
"code": "SUCCESS",
"data": {
"orderId": "ORD-123",
"productId": "SKU-1001",
"quantity": 2,
"status": "CREATED"
}
}
实际业务中,字段、状态码和状态流转需要以接口契约为准。
五、Collection 如何组织,决定了后续维护成本
建议以业务能力或测试目的组织,而不是把几十个接口平铺在一个 Collection 里:
text
Orders API
├── 01 Login
├── 02 Create order
├── 03 Get order
└── 04 Cancel order
本文的示例 Collection 只包含订单链路,后面的 Newman 配置会执行整个 Collection。冒烟测试和负向测试分别放在其他 Collection 中,并根据需要单独执行。 这里有一个重要取舍:
- 独立接口测试:用来覆盖参数、权限和异常分支,尽量减少前置依赖。
- 业务链路测试:用来验证跨接口状态流转,允许存在明确的上下游依赖。
不要让全部测试都串成一条超长链路。否则第一个登录接口失败,后面几十个用例都跟着报错,最终只得到一堆没有诊断价值的失败。
请求命名建议
请求名最好表达业务动作:
text
Create order - valid product
Create order - missing productId
Get order - existing order
Cancel order - created order
断言名同样要能直接读懂:
javascript
pm.test("创建订单:业务码为 SUCCESS", function () {
// ...
});
当 Jenkins 中出现失败时,工程师应该能从测试名初步判断问题,而不是只能看到 test 1 failed。
六、变量作用域:很多"本地能跑,CI 跑不通"的根源
Postman 变量存在多个作用域。对于同名变量,通常按作用域由宽到窄覆盖:
text
Global
↓
Collection
↓
Environment
↓
Data
↓
Local
越靠近当前执行上下文的变量,优先级越高。官方文档将其描述为 global、collection、environment、data、local 的顺序。Postman 变量作用域文档
例如:
text
Collection: baseUrl = https://dev.example.internal
Environment: baseUrl = https://test.example.internal
Data: baseUrl = https://unexpected.example.internal
最终请求可能会发往数据文件中的地址。
这就是为什么我建议:
| 数据 | 推荐位置 |
|---|---|
| 测试环境地址 | 环境变量,由 CI 明确注入 |
| 用户名、密码、Token | CI 凭据系统或秘密管理系统 |
| 商品、数量、预期值 | 迭代数据文件 |
| 当前流程生成的订单 ID | Collection 运行时变量 |
| 固定且不敏感的默认值 | Collection 变量 |
不要在数据文件里放 baseUrl、账号、密码等运行环境配置,避免意外覆盖。
还要区分两种访问方式:
javascript
pm.variables.get("orderId")
读取当前解析结果,会受多个作用域优先级影响。
javascript
pm.collectionVariables.get("orderId")
明确读取 Collection 作用域。
对于链路中生成的中间状态,后者更容易理解和排查。
七、第一步:登录并提取 Token
1. 配置登录请求
http
POST {{baseUrl}}/api/auth/login
Content-Type: application/json
Body 选择 raw、JSON,初始内容填写 {}。在登录请求的 Pre-request script 中构造请求体:
javascript
const username = pm.environment.get("username");
const password = pm.environment.get("password");
pm.request.body.update(JSON.stringify({ username, password }));
username 和 password 由 Jenkins 在运行时注入,不写进 Collection 和 Git。使用 JSON.stringify 构造请求体,可以正确处理凭据中的引号、反斜杠和换行等特殊字符。不要在脚本或日志中打印请求体。
2. 在 Post-response 中验证并保存 Token
javascript
let body = null;
try {
body = pm.response.json();
} catch (err) {
// 后面的测试会报告响应格式错误。
}
pm.test("登录:HTTP 状态码为 200", function () {
pm.response.to.have.status(200);
});
pm.test("登录:响应是合法 JSON", function () {
pm.expect(body).to.be.an("object");
});
pm.test("登录:业务码为 SUCCESS", function () {
pm.expect(body && body.code).to.eql("SUCCESS");
});
pm.test("登录:返回非空 accessToken", function () {
pm.expect(body && body.data && body.data.accessToken)
.to.be.a("string")
.and.not.empty;
});
const loginSucceeded =
pm.response.code === 200 &&
body &&
body.code === "SUCCESS" &&
body.data &&
typeof body.data.accessToken === "string" &&
body.data.accessToken.length > 0;
if (loginSucceeded) {
pm.collectionVariables.set("accessToken", body.data.accessToken);
} else {
// 避免沿用上一次迭代或历史运行中残留的 Token。
pm.collectionVariables.unset("accessToken");
// 终止当前迭代的后续链路,避免产生大量级联失败。
pm.execution.setNextRequest(null);
}
这里有两个细节:
- 先记录断言失败,再终止链路。 单纯停止执行不一定等同于测试失败。
- 失败时清理旧 Token。 否则数据驱动运行时,后续迭代可能误用上一轮登录产生的状态。
pm.execution.setNextRequest() 只在 Collection 运行中生效;设置为 null 会结束当前迭代的后续请求,多次迭代时并不等于结束所有迭代。Postman 工作流文档
八、第二步:创建订单,并把 ID 传给后续请求
1. 配置鉴权
可以在 Collection 级别配置 Bearer Token:
text
{{accessToken}}
创建、查询和取消订单请求继承 Collection 鉴权;登录请求单独选择 No Auth,不继承 Bearer Token。
请确保 Collection 中没有同名的环境变量或数据变量意外覆盖这个 Token。
2. 创建请求
http
POST {{baseUrl}}/api/orders
Content-Type: application/json
Authorization: Bearer {{accessToken}}
Pre-request script:
javascript
// 每次创建订单前清理上次运行残留的 ID。
pm.collectionVariables.unset("orderId");
// 为本次创建生成唯一业务编号。
const clientOrderNo =
"autotest-" + pm.variables.replaceIn("{{$guid}}");
pm.collectionVariables.set("clientOrderNo", clientOrderNo);
Body:
json
{
"productId": "{{productId}}",
"quantity": {{quantity}},
"clientOrderNo": "{{clientOrderNo}}"
}
这里的 quantity 直接作为 JSON 数字插入,因此测试数据必须提供合法数字。更复杂的字符串转义、嵌套对象或非法输入测试,建议使用更可控的请求构造方式,避免模板替换后产生无效 JSON。
3. 添加断言和上下文提取
javascript
let body = null;
try {
body = pm.response.json();
} catch (err) {
// 由测试报告 JSON 解析失败。
}
pm.test("创建订单:HTTP 状态码为 201", function () {
pm.response.to.have.status(201);
});
pm.test("创建订单:响应是合法 JSON", function () {
pm.expect(body).to.be.an("object");
});
pm.test("创建订单:业务码为 SUCCESS", function () {
pm.expect(body && body.code).to.eql("SUCCESS");
});
pm.test("创建订单:返回非空 orderId", function () {
pm.expect(body && body.data && body.data.orderId)
.to.be.a("string")
.and.not.empty;
});
const created =
pm.response.code === 201 &&
body &&
body.code === "SUCCESS" &&
body.data &&
typeof body.data.orderId === "string" &&
body.data.orderId.length > 0;
if (created) {
pm.collectionVariables.set("orderId", body.data.orderId);
} else {
pm.collectionVariables.unset("orderId");
pm.execution.setNextRequest(null);
}
为什么要使用唯一业务编号?
自动化任务可能在以下情况下重复执行:
- 开发者本地调试;
- Jenkins 手动重跑;
- 定时任务;
- 多个分支同时构建;
- 上游流程失败后重新触发。
如果固定使用同一个 clientOrderNo,测试可能因为唯一索引、幂等校验或历史数据而失败。
但需要注意:唯一编号不等于幂等方案。如果创建请求超时,服务端可能已经创建成功。盲目换一个编号重试,可能生成两笔订单。对于重试,应当使用服务端支持的幂等键,并在一次业务操作的重试中保持相同值。
九、第三步:查询订单,验证业务语义
http
GET {{baseUrl}}/api/orders/{{orderId}}
Authorization: Bearer {{accessToken}}
Post-response:
javascript
let body = null;
try {
body = pm.response.json();
} catch (err) {
// 由测试报告格式错误。
}
const expectedProductId = pm.iterationData.get("productId");
const expectedQuantity = Number(pm.iterationData.get("quantity"));
const expectedOrderId = pm.collectionVariables.get("orderId");
pm.test("查询订单:HTTP 状态码为 200", function () {
pm.response.to.have.status(200);
});
pm.test("查询订单:业务码为 SUCCESS", function () {
pm.expect(body && body.code).to.eql("SUCCESS");
});
pm.test("查询订单:订单 ID 与创建结果一致", function () {
pm.expect(body && body.data && body.data.orderId)
.to.eql(expectedOrderId);
});
pm.test("查询订单:商品与请求一致", function () {
pm.expect(body && body.data && body.data.productId)
.to.eql(expectedProductId);
});
pm.test("查询订单:数量与请求一致", function () {
pm.expect(body && body.data && body.data.quantity)
.to.eql(expectedQuantity);
});
pm.test("查询订单:初始状态为 CREATED", function () {
pm.expect(body && body.data && body.data.status)
.to.eql("CREATED");
});
这比单纯验证 200 更接近真实需求:创建出来的到底是不是我们想要的订单?
对最终一致性系统,不要机械地立即断言
如果创建接口写入数据库后,查询接口依赖异步索引或消息消费,立即查询可能出现短暂的"查不到"。
正确做法不是无条件加一个很长的固定 sleep,而是根据业务契约采用:
- 有上限的轮询;
- 固定或递增的轮询间隔;
- 明确的超时;
- 超时后输出最后一次可诊断的状态;
- 只对明确允许最终一致性的场景使用。
同时要区分:轮询是在等待已定义的最终一致性,还是在掩盖服务不稳定。
十、第四步:数据驱动,而不是复制十份请求
创建 data/orders.json:
json
[
{
"productId": "SKU-1001",
"quantity": 1
},
{
"productId": "SKU-1002",
"quantity": 2
}
]
这个数据文件只保存需要多次运行的输入数据;请求定义、流程和断言仍维护在 Collection 中。不需要数据驱动时,可以不提供数据文件,让 Collection 按默认配置执行。
Newman 使用这个文件时,会按数据行执行 Collection 迭代。每一轮可以通过:
javascript
pm.iterationData.get("productId")
读取当前数据。
这里需要区分两个概念:
text
两条数据
≠
同一个请求简单执行两次
如果运行的是整个业务链路,那么每条数据通常都会执行一遍完整流程:
text
第 1 轮:登录 → 创建 SKU-1001 → 查询 → 取消
第 2 轮:登录 → 创建 SKU-1002 → 查询 → 取消
因此,数据驱动之前先确认:
- 每一轮是否都需要登录?
- 每轮数据是否相互独立?
- 上一轮的 Token、订单 ID 等状态是否会残留?
- 测试账号是否允许多轮或并发操作?
- 测试商品是否有足够库存?
Postman 对 JSON 和 CSV 数据文件都有格式要求;数据字段名区分大小写。Postman 数据文件文档
十一、清理测试数据:取消订单不一定等于彻底清理
最后一个请求可以调用取消接口:
http
POST {{baseUrl}}/api/orders/{{orderId}}/cancel
Authorization: Bearer {{accessToken}}
并断言取消成功。
但需要正视一个问题: 把清理请求放在链路最后,不代表所有情况下清理一定会执行。
例如:
- 创建成功后,查询请求发生网络错误;
- Jenkins Agent 被终止;
- 流水线超时;
- 脚本异常中断;
- 测试执行到一半机器重启。
这时清理请求可能根本没有机会运行。
更可靠的做法通常是组合使用:
- 业务流程内清理:正常执行时取消或删除资源。
- 测试数据标识:使用 autotest- 前缀、专属账号或标签。
- 定时兜底清理:清理超过 TTL 的测试数据。
- 环境隔离:使用专门的测试租户、数据库或临时环境。
- 受控清理接口:仅在测试环境开放,并限制权限。
清理任务必须有严格的作用范围,不能仅凭宽泛的时间条件删除业务数据。
十二、为什么本文用 Node.js 调 Newman,而不是只写一条命令?
最简单的 Newman 命令如下:
bash
npx newman run collections/orders.postman_collection.json \
--iteration-data data/orders.json \
--reporters cli,junit \
--reporter-junit-export reports/newman.xml
这条命令只展示参数结构,并未注入本例必需的 baseUrl 和登录凭据,不能直接运行完整示例。本地验证请使用后面的 Node.js 入口和环境变量配置。
但当我们要把 Jenkins 凭据注入到 Newman 时,如果直接把密码拼到命令参数:
bash
newman run collection.json \
--env-var "password=真实密码"
密码可能暴露在进程参数、日志或诊断信息中。
因此,这里使用 Newman 的 Node.js API,从进程环境变量中读取凭据,再通过 envVar 传入。Newman 官方支持以库的形式调用,并提供运行摘要、报告和变量覆盖配置。Newman 官方文档
这并不意味着环境变量是绝对安全的:同一执行节点上的其他进程可能在某些条件下读取环境信息。敏感任务仍应使用可信 Agent、隔离执行环境、最小权限测试账号,并控制日志和产物访问。 scripts/run-newman.js
javascript
"use strict";
const fs = require("fs");
const path = require("path");
const newman = require("newman");
const requiredEnv = [
"API_BASE_URL",
"API_USERNAME",
"API_PASSWORD"
];
const missing = requiredEnv.filter((name) => !process.env[name]);
if (missing.length > 0) {
console.error(`缺少必要环境变量: ${missing.join(", ")}`);
process.exit(2);
}
const rootDir = path.resolve(__dirname, "..");
const reportDir = path.join(rootDir, "reports");
const reportPath = path.join(reportDir, "newman.xml");
fs.mkdirSync(reportDir, { recursive: true });
// 防止上一次运行的报告被误认为是本次结果。
if (fs.existsSync(reportPath)) {
fs.unlinkSync(reportPath);
}
newman.run(
{
collection: path.join(
rootDir,
"collections",
"orders.postman_collection.json"
),
// 提供一个运行时 environment 容器。
environment: {
name: "ci-runtime",
values: []
},
envVar: [
{
key: "baseUrl",
value: process.env.API_BASE_URL
},
{
key: "username",
value: process.env.API_USERNAME
},
{
key: "password",
value: process.env.API_PASSWORD
}
],
iterationData: path.join(rootDir, "data", "orders.json"),
reporters: ["cli", "junit"],
reporter: {
junit: {
export: reportPath
}
},
// 以下仅为示例阈值,应根据服务实际 SLO 和 CI 环境调整。
timeoutRequest: 10_000,
timeoutScript: 5_000,
timeout: 180_000,
workingDir: rootDir,
insecureFileRead: false,
color: "off"
},
function (err, summary) {
if (err) {
console.error("Newman 启动或运行失败:", err.message);
process.exitCode = 1;
return;
}
const run = summary && summary.run;
const stats = run && run.stats;
const failures = run && run.failures;
if (!stats || !Array.isArray(failures)) {
console.error("Newman 未返回完整运行摘要,无法确认测试结果");
process.exitCode = 1;
return;
}
const requestCount = stats.requests && stats.requests.total;
const assertionCount = stats.assertions && stats.assertions.total;
if (!Number.isInteger(requestCount) || requestCount < 1 ||
!Number.isInteger(assertionCount) || assertionCount < 1) {
console.error("未检测到有效请求或断言,拒绝将空测试判定为成功");
process.exitCode = 1;
return;
}
console.log(
`请求数: ${requestCount},断言数: ${assertionCount},失败项: ${failures.length}`
);
if (failures.length > 0) {
process.exitCode = 1;
}
}
);
在 package.json 中增加:
json
{
"scripts": {
"test:api": "node scripts/run-newman.js"
}
}
实际修改时,保留 npm init 和安装依赖生成的其他字段,不要用上面的片段覆盖整个文件。
为什么显式检查 summary.run.failures?
因为这里使用的是 Newman 的库接口。我们需要主动把运行摘要中的失败映射到 Node 进程退出码,保证 Jenkins 能看到非零状态。
同时,初始化错误与测试断言失败是不同的失败类型:
- err:Newman 初始化或运行层面的错误;
- summary.run.failures:请求、脚本、断言等执行过程中的失败项。
除了失败项,还要检查运行摘要、请求数和断言数。空 Collection 或没有断言的请求不能证明测试通过,因此示例将这两种情况判为失败。对于固定业务链路,团队还可以结合迭代数校验预期请求和断言数量,避免只执行了部分流程却显示成功。
不要只打印结果却始终返回 0。那样测试全部失败,Jenkins 仍可能显示绿色。
十三、本地执行:先让 CI 命令在开发机跑通
以 macOS / Linux 为例:
bash
export API_BASE_URL="https://test.example.internal"
export API_USERNAME="your-test-account"
read -s "API_PASSWORD?Test password: "
export API_PASSWORD
npm ci
npm run test:api
unset API_PASSWORD
上面使用的是 zsh 的 read -s 写法;其他 shell 的交互式输入语法可能不同。
执行后检查:
text
reports/newman.xml
预期结果:
- 所有断言通过:进程退出码为 0;
- 存在断言或请求失败:进程退出码为非零;
- 缺少必需配置:执行入口直接失败;
- 报告输出到固定路径,供 Jenkins 收集。
不要把生产环境地址和生产账号用于示例跑通。 优先使用隔离测试环境与低权限测试账号。
十四、Jenkins 落地:让失败真正阻断流水线
1. 前置条件
Jenkins 需要具备:
- 能访问 Git 仓库;
- Agent 已安装并固定项目兼容的 Node.js 和 npm;
- 能访问目标测试环境;
- 已安装支持 junit 步骤的插件;
- Jenkins 凭据中存在一个"用户名和密码"类型的凭据,例如 api-test-user。
Jenkins 的 Credentials Binding 支持在有限作用域内将凭据绑定为环境变量。即使 Jenkins 会尝试遮蔽日志,也不应依赖遮蔽机制抵御有意或无意的泄漏。Jenkins 凭据绑定文档
2. Jenkinsfile
groovy
pipeline {
agent {
label 'node-api-test'
}
options {
timestamps()
timeout(time: 15, unit: 'MINUTES')
skipDefaultCheckout(true)
}
parameters {
choice(
name: 'TEST_ENV',
choices: ['test', 'staging'],
description: '选择允许执行接口测试的环境'
)
}
stages {
stage('Checkout') {
steps {
deleteDir()
checkout scm
}
}
stage('Install dependencies') {
steps {
sh '''
set -eu
node --version
npm --version
npm ci
'''
}
}
stage('API tests') {
steps {
script {
def targets = [
test : 'https://test.example.internal',
staging: 'https://staging.example.internal'
]
def targetUrl = targets[params.TEST_ENV]
if (!targetUrl) {
error("不允许的测试环境: ${params.TEST_ENV}")
}
withEnv(["API_BASE_URL=${targetUrl}"]) {
withCredentials([
usernamePassword(
credentialsId: 'api-test-user',
usernameVariable: 'API_USERNAME',
passwordVariable: 'API_PASSWORD'
)
]) {
sh '''
set +x
npm run test:api
test -s reports/newman.xml
'''
}
}
}
}
}
}
post {
always {
script {
// 成功路径要求有可解析的非空测试结果;失败路径尽力收集已有报告。
def alreadyFailed = currentBuild.currentResult != 'SUCCESS'
junit(
testResults: 'reports/newman.xml',
allowEmptyResults: alreadyFailed
)
}
}
}
}
这段流水线有几个关键点
1. 使用环境白名单
流水线只允许选择预先配置的环境,而不是让用户任意输入 URL。
这可以减少误打生产环境和向非预期地址发送测试凭据的风险。实际项目还应结合 Jenkins 权限、网络策略和测试账号权限控制。
2. 不在 Groovy 字符串中插入密码
密码通过 withCredentials 提供给子进程。Shell 脚本使用单引号多行字符串,避免 Groovy 提前插值。
同时使用:
bash
set +x
关闭 shell 命令回显。但请注意:这不能阻止应用代码、脚本或报告主动打印敏感内容。
3. post { always { ... } } 收集失败报告
接口测试失败时,流水线通常已经进入失败状态,但我们仍然希望 Jenkins 读取 JUnit XML,展示失败用例和趋势。
成功路径中,test -s 要求报告文件存在且非空,junit 进一步要求报告能够解析并包含测试结果;测试或准备阶段已经失败时,允许报告缺失,避免掩盖原始错误。报告检查不能替代测试退出码判断,两者需要同时具备。
4. 把测试接入发布链路
上面的 Jenkinsfile 是独立的接口测试流水线。若要作为发布门禁,需要将测试阶段接入部署后的发布链路,或者让发布流水线触发并等待该测试任务,确保测试失败时阻断后续发布。同时要确认目标环境运行的是本次构建对应的服务版本。
5. 不把完整响应默认归档
Newman 的 JSON 报告、日志以及部分 HTML 报告可能包含请求、响应、业务数据或认证信息。是否归档应该基于报告内容、脱敏情况和 Jenkins 访问权限决定。
JUnit XML 也不是天然无敏感信息:如果把响应正文或秘密拼进断言消息,同样可能泄漏。
十五、测试报告:JUnit 解决趋势,详细报告解决排障
Newman 内置多种 reporter,包括:
- cli
- json
- junit
- progress
JUnit XML 适合接入 Jenkins 的测试结果视图;JSON 更适合二次加工,但内容也更全面,需要谨慎控制保存和访问。Newman Reporter 文档
如果使用第三方 HTML reporter,需要额外确认:
- reporter 包是否安装并锁定版本;
- 报告是否包含请求头、Token、Cookie 或响应正文;
- 报告是否被 Jenkins 归档;
- 报告的访问权限和保留周期;
- reporter 与当前 Newman 版本是否兼容。
常见误区是把"生成了漂亮的 HTML"当成测试体系已经完成。
真正有价值的报告应该至少回答:
- 哪个环境?
- 哪个代码版本?
- 哪条业务链路?
- 哪一个断言失败?
- 预期是什么?
- 实际是什么?
- 失败属于请求、脚本还是业务断言?
- 是否能关联服务端 traceId?
建议在不泄露敏感信息的前提下,将请求中的 traceId、业务编号等可检索信息输出到日志或测试上下文,方便从 Jenkins 跳转到服务端日志和链路追踪系统。
十六、断言设计:从"能跑"走向"能发现问题"
1. 不要只断言字段存在
较弱的断言:
javascript
pm.expect(body.data.orderId).to.exist;
问题在于,它可能无法准确表达你的业务约束。
更明确:
javascript
pm.expect(body.data.orderId)
.to.be.a("string")
.and.not.empty;
2. 把协议失败与业务失败分开
javascript
pm.test("HTTP 状态码为 201", function () {
pm.response.to.have.status(201);
});
pm.test("业务码为 SUCCESS", function () {
pm.expect(body && body.code).to.eql("SUCCESS");
});
这样失败报告能区分:
- 网关返回了错误状态;
- HTTP 层成功,但业务失败。
3. 校验输入与输出之间的关系
javascript
pm.expect(body.data.productId).to.eql(
pm.iterationData.get("productId")
);
这比只验证 productId 存在更有意义。
4. 不要对不稳定字段做精确匹配
例如:
javascript
pm.expect(body.data.createdAt).to.eql("2026-11-10T10:00:00Z");
如果时间由服务端生成,这样的断言通常没有意义。
更合理的是验证:
- 字段存在;
- 格式符合约定;
- 时间位于合理区间;
- 或验证多个时间字段之间的业务关系。
5. Schema 校验与业务断言各司其职
Schema 适合验证:
- 字段是否存在
- 字段类型是否正确
- 数组和对象结构是否符合约定
- 枚举值是否在允许范围
业务断言适合验证:
- 支付金额是否正确
- 状态是否发生预期流转
- 用户是否只能访问自己的数据
- 重复请求是否符合幂等约定
不要指望一个 Schema 覆盖全部业务正确性。
十七、失败策略:让一次失败尽可能有诊断价值
测试失败并不都是同一类问题。
| 失败类型 | 常见原因 | 处理方向 |
|---|---|---|
| 环境失败 | DNS、网络、服务不可用 | 检查 Agent 到服务的连通性 |
| 配置失败 | 缺少变量、地址错误、凭据失效 | 检查 Jenkins 参数与凭据 |
| 协议失败 | 状态码、响应格式不符合契约 | 检查网关与服务日志 |
| 业务失败 | 业务码或状态流转不符合预期 | 检查业务逻辑与测试数据 |
| 数据失败 | 库存不足、账号冲突、历史数据污染 | 检查数据准备与隔离 |
| 脚本失败 | JSON 解析错误、变量未定义 | 检查 Collection 脚本 |
| 不稳定失败 | 竞争条件、异步延迟、共享资源 | 定位根因并控制并发 |
是否使用 --bail?
Newman 支持在特定失败条件下提前停止。是否使用取决于测试类型:
- 冒烟测试:核心前置条件失败后提前退出,缩短反馈时间。
- 独立回归测试:尽量继续执行,收集更多失败信息。
- 业务链路测试:前置数据创建失败后停止当前链路,避免级联报错。
本文示例针对登录和订单创建显式终止当前迭代,而没有对整套执行统一启用提前退出。
同样,不要使用 --suppress-exit-code 作为正常发布门禁。它会让 Newman 忽略部分运行失败所对应的退出状态,容易造成"测试失败但流水线成功"。Newman CLI 参数文档
十八、并发与隔离:测试任务跑多了之后才暴露的问题
Newman 可以通过多个进程或多个 Jenkins 任务并行执行,但并发本身不会自动解决数据隔离。
典型问题包括:
text
任务 A 和任务 B 使用同一账号
↓
任务 B 登录导致任务 A 的 Token 失效
↓
任务 A 后续请求返回 401
↓
表面看是服务鉴权不稳定,实际是测试账号互相影响
或者:
text
两个任务同时购买同一个测试 SKU
↓
库存不足
↓
一个任务随机失败
并发前至少检查:
- 每个执行单元是否有独立测试账号或会话?
- 订单号、用户名等唯一字段是否隔离?
- 测试资源是否足够?
- 清理任务是否会误删其他执行中的数据?
- 报告文件是否写入各自独立的工作目录?
- 下游服务是否允许测试流量并发?
如果使用 Jenkins 并行阶段,建议为每个分片使用独立工作目录和报告路径,例如:
text
reports/shard-1/newman.xml
reports/shard-2/newman.xml
然后统一收集:
groovy
junit 'reports/**/newman.xml'
并行化的前提是测试本身可隔离;否则只是把原本低概率出现的问题放大。
十九、重试不是止痛药:先判断能不能重试
接口测试中经常出现这种代码: 失败就重试 3 次 它可能短期降低红灯数量,却同时掩盖真实问题。
重试前至少考虑三个维度:
1. 失败是否具有暂时性?
相对可能适合有限重试:
- 明确的瞬时网络中断;
- 已知最终一致性窗口内的查询;
- 服务端明确返回可重试错误。
通常不适合直接重试:
- 参数错误;
- 权限错误;
- 业务断言失败;
- 数据契约不匹配。
2. 操作是否可安全重试?
- GET 查询:通常更容易重试
- POST 创建订单:可能造成重复数据
- POST 支付:必须严格遵循业务幂等设计
HTTP 方法只能提供初步判断,最终要以接口实际语义和幂等契约为准。
3. 重试是否被记录?
即使重试后成功,也建议记录:
- 首次失败原因;
- 重试次数;
- 总耗时;
- 最终结果。
否则不稳定性会被"绿色构建"隐藏。
二十、超时配置:不要只设一个 Jenkins 总超时
接口自动化至少有三个层次的超时:
text
单个 HTTP 请求超时
<
单次 Collection 运行超时
<
Jenkins Stage / Pipeline 超时
例如:
text
单请求:10 秒
Collection:180 秒
Pipeline:15 分钟
这只是示例,不是通用推荐值。
还要考虑:
- Collection 包含多少请求和迭代;
- 每次请求是否会等待异步事件;
- 接口本身的服务等级目标;
- Jenkins Agent 的网络质量;
- 报告生成与依赖安装耗时。
测试中也可以对响应时间设置断言,但要谨慎:
javascript
pm.test("响应时间低于 1500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(1500);
});
这种断言适合作为粗粒度的异常检测,不适合作为严肃的性能测试结论。共享测试环境、网络抖动和 CI 负载都会影响结果。吞吐量、百分位延迟和容量评估应使用专门的性能测试工具与受控环境。
二十一、Postman 和 Newman 的兼容性:不要假设桌面端能跑,CI 就一定能跑
Postman 产品和运行时在持续演进,不同执行入口的能力并不完全一致。
一个明确例子是:
javascript
pm.execution.runRequest(...)
Postman 官方文档明确说明:该方法不受 Newman 支持。如果 Collection 依赖它,本地在 Postman 中可执行,不代表 Newman 可以执行。Postman 脚本执行参考
因此,团队最好建立一条规则: Collection 合并前,必须使用与 CI 相同版本的 Newman 在命令行执行。
对依赖较新的脚本 API、Vault、第三方包或特殊认证能力的 Collection,尤其要先验证目标运行时是否支持。
如果团队大量依赖 Newman 不支持的新功能,也可以评估 Postman CLI;但不要假设两者的参数、认证方式和运行行为完全相同,迁移前应逐项验证。
二十二、如何把它变成真正的质量门禁?
不是所有测试都应该放在每次代码提交的关键路径上。
一种常见分层方式:
| 测试层 | 触发时机 | 目标耗时 | 策略 |
|---|---|---|---|
| 冒烟测试 | 部署到测试环境后 | 分钟级 | 核心流程失败立即阻断 |
| 核心回归 | 合并主干或部署预发后 | 按团队容量设定 | 覆盖关键业务链路 |
| 全量回归 | 夜间或专用流水线 | 可相对更长 | 覆盖更多异常与边界 |
| 契约测试 | 服务变更或合并请求 | 尽量快速 | 尽早发现接口兼容性问题 |
具体触发方式要与团队的部署流程匹配。尤其需要确认:
- 测试环境运行的服务版本是否对应当前构建?
- 测试失败是否会真正阻断发布?
- 是否存在多人共享环境导致的版本覆盖?
- 测试结果能否追溯到 Git commit、环境和构建编号?
如果代码是 A 版本,测试环境实际上运行 B 版本,那么测试结果再漂亮也不能证明 A 可发布。
二十三、上线前,如何验证这套门禁真的有效?
不要只验证"正常情况下能跑通"。至少做一次受控的失败演练:
- 正常运行:确认测试通过,Jenkins 展示测试结果。
- 人为制造断言失败:例如临时修改预期业务码,确认进程非零退出,流水线失败。
- 使用错误凭据:确认登录失败,并且不会继续执行创建订单。
- 使用不可达地址或关闭测试服务:确认网络失败能被识别。
- 删除或破坏 Collection 路径:确认初始化失败不会显示为成功。
- 检查日志与报告:确认没有明文密码、Token 和不必要的业务数据。
- 并发运行两次:检查账号、库存、唯一编号和清理是否互相影响。
- 中途中断一次运行:确认兜底清理机制能够处理残留数据。
这些演练的价值在于证明:系统不仅会在成功时亮绿灯,也会在应该失败时可靠地亮红灯。
写在最后
这套 Postman + Newman + Jenkins 的真正价值,不在于把 Postman 里的请求搬到服务器上执行,而在于建立一条可追溯、可复现、可诊断的质量反馈链路:
text
接口契约
→ 可维护的 Collection
→ 可隔离的测试数据
→ 可重复执行的 Newman 入口
→ 可观测的 Jenkins 报告
→ 真正生效的发布门禁
当团队能够稳定回答"测了哪个版本、哪些业务、为什么失败、数据如何清理"时,接口自动化才算真正进入工程化阶段。
写这篇文章的初衷,是想分享一种结合团队现有工具和工作习惯落地接口自动化的思路。方案选型没有统一答案:如果团队有成熟的代码框架和维护能力,代码驱动同样是很好的选择;如果团队已经积累了大量 Postman Collection,并且希望降低上手门槛,本文的方案也许值得尝试。
无论选择哪种工具,最终都要看它是否适合团队的业务复杂度、协作方式和长期维护能力。我是 Harvey,respect...