Postman 学习笔记 IV:Workflow、Newman 与 Mock Server 实战技巧
这一篇的内容因为是收尾作,相对而言比较散,基本上就是吧零零散散的,之前没有提到过的功能收一下尾了
之前的笔记在:
这一篇主要会涉及一些自动化和 CI/CD 的部分,repo 地址依旧在:
https://github.com/GoldenaArcher/postman-api-study
workflow
postman 中的流程,虽然叫 workflow,不过从实践上来说,最多只是一个半自动流程
在比较早期的版本中,postman 曾经提供过一个图像化管理 workflow 的功能,通过拖拽进行排列,不过我找了下最近的版本,确实没看到这个功能,如果不是藏得太深了,就可能已经 discontinue 了
目前主流的方法还是通过 postman 提供的方法, pm.execution.setNextRequest()
,去指定下一个要运行的 request。 setNextRequest
中的参数可以是 request name,也可以是 request id,这部分的信息可以在 info 页面查看:

或者使用 pm.info.requestId
和 pm.info.requestName
获取:

workflow 只在 Collection Runner, Newman 和 Monitor 中工作,如果只是单纯的点击 send
进行 request,那么它是不起效的
一些用法包括:
jsx
if (pm.response.code === 401) {
pm.execution.setNextRequest("Login Request"); // retry with login
} else {
pm.execution.setNextRequest("Get Orders"); // continue normal flow
}
或者:
jsx
if (pm.response.json().status !== "READY") {
pm.execution.setNextRequest("Check Job Status");
} else {
pm.execution.setNextRequest("Get Final Result");
}
⬆️ 这种情况可能比较多的会用在 while
这种尝试 re-attempt,用来测试 mutation 是否起效
或者:
jsx
pm.execution.setNextRequest(null);
⬆️ 这个写法可以终止 workflow
循环测试
这种实现方法大体如下:
jsx
var i = !pm.variables.get("i") ? 1 : pm.variables.get("i");
if (i < 3) {
console.log(
"this is run " + i + " for request " + pm.execution.location.current
);
pm.execution.setNextRequest(pm.execution.location.current);
i++;
pm.variables.set("i", i);
} else {
pm.execution.setNextRequest(null);
}
相当于不断请求自身,进行 loop
跨请求协同合作
这种比较多的是将状态保存在 global environment 中,然后通过 pm.global.set
和 pm.global.get
进行跨请求合作。如下面的 3 个请求的测试:
-
Request A
jsxconst data = pm.response.json().items; pm.globals.set("itemsList", JSON.stringify(data)); pm.globals.set("loopIndex", "0"); pm.execution.setNextRequest("Request B");
-
Request B
jsxlet list = JSON.parse(pm.globals.get("itemsList")); let idx = parseInt(pm.globals.get("loopIndex"), 10); if (idx < list.length) { pm.environment.set("currentItem", list[idx]); pm.globals.set("loopIndex", String(idx + 1)); pm.execution.setNextRequest("Request B"); } else { pm.execution.setNextRequest("Request C"); }
-
Request C
jsxpm.globals.unset("itemsList"); pm.globals.unset("loopIndex"); pm.execution.setNextRequest(null);
大体上的流程是这样的:
loopIndex=0
while loopIndex < list.length
idx++ loopIndex = list.length cleanup Request A Request B Request C 结束
数据文件
这里是可以上传数据文件------JSON/CSV 格式,然后 postman 会从文件中读取数据跑迭代:

获取数据的方式为: pm.iterationData.get
但是,这个在具体跑的时候会有一点坑:
-
每一条数据代表这一个迭代
以下面的 JSON 为例:json[ { "testName": "Get all products", "endpoint": "/products", "expectedStatus": 200, "description": "Should return all products in the catalog" }, { "testName": "Get single product", "endpoint": "/products/1", "expectedStatus": 200, "description": "Should return details for a single product with ID = 1" }, { "testName": "Get all products valid category", "endpoint": "/products?category=electronics", "expectedStatus": 200, "description": "Should return only products belonging to the electronics category" }, { "testName": "Get all products invalid category", "endpoint": "/products?category=invalid-category", "expectedStatus": 400, "description": "Should return error for invalid category filter" } ]
这里假设所有的数据,即
testName
,endpoint
等都会从pm.iterationData.get
获取
本质上来说,这里提供了 5 条数据,整个 collection 会跑 5 遍,假设这个 collection 有 20 个 endpoints,就是说它跑了 20 × 5 20 \times 5 20×5 ,也就是 100 条请求
而在每个迭代中:
所有的请求都会测试对应的endpoint
这一条,具体会有多少条数据成功,不可判断。以第一个迭代为例,但是只有原本的Get all products
可以完美匹配,其他的请求,如原本的 get categories ,status code、返回类型为数组这种基础测试可以通过,但是验证返回类型(schema 或 object type)会失败;测试返回一条数据的基本只会有 status code 是成功的,测试 false case 的大概率全挂,因为 status code 会报错
同理,当跑第二个迭代时,只有原本的Get single product
可以完美匹配。可以看到,这里会有很多 false alarm 的情况
一个处理方法是通过判断条件,动态从 environment variable 中获取数据:jsxif (pm.iterationData.get("testName") !== "Get all products") { pm.execution.setNextRequest(null); // 或直接 return,跳过 }
但是从这个角度上来说,虽然整个 test runner 跑了 5 遍 iteration,本质上还是 1 遍 data file 中的数据+4 遍 environment variable 中的数据,本质上没有更加的灵活地迭代
-
JSON 数据的管理困难
另一种解决方法是修改 JSON 文件,去更好地匹配每个迭代,如下面这两个 JSON:json[ { "iteration": 1, "tests": [ { "testName": "Get all products", "endpoint": "/products", "expectedStatus": 200, "description": "Should return all products in the catalog" }, { "testName": "Get single product", "endpoint": "/products/1", "expectedStatus": 200, "description": "Should return details for a single product with ID = 1" }, { "testName": "Get all products valid category", "endpoint": "/products?category=electronics", "expectedStatus": 200, "description": "Should return only products belonging to the electronics category" } ] }, { "iteration": 2, "tests": [ { "testName": "Get all products invalid category", "endpoint": "/products?category=invalid-category", "expectedStatus": 400, "description": "Should return error for invalid category filter" }, { "testName": "Get single product not found", "endpoint": "/products/9999", "expectedStatus": 404, "description": "Should return not found for non-existent product" }, { "testName": "Get all products another category", "endpoint": "/products?category=books", "expectedStatus": 200, "description": "Should return only products belonging to the books category" } ] } ]
以及
json[ { "iteration": 1, "tests": { "Get all products": { "endpoint": "/products", "expectedStatus": 200, "description": "Should return all products in the catalog" }, "Get single product": { "endpoint": "/products/1", "expectedStatus": 200, "description": "Should return details for a single product with ID = 1" }, "Get all products valid category": { "endpoint": "/products?category=electronics", "expectedStatus": 200, "description": "Should return only products belonging to the electronics category" } } }, { "iteration": 2, "tests": { "Get all products invalid category": { "endpoint": "/products?category=invalid-category", "expectedStatus": 400, "description": "Should return error for invalid category filter" }, "Get single product not found": { "endpoint": "/products/9999", "expectedStatus": 404, "description": "Should return not found for non-existent product" }, "Get all products another category": { "endpoint": "/products?category=books", "expectedStatus": 200, "description": "Should return only products belonging to the books category" } } } ]
这二者的实现其实是一样的,不过其中的
tests
一个是 array based 一个是 object based,换言之,一个在测试中通过pm.iterationData.get("tests")
去找匹配的数据,一个可以直接通过get
获取
不过二者都暴露了一个问题,那就是当 endpoints 数量比较大时,data file 的管理是非常困难的,尤其涉及到修改测试文件数据的情况 -
无法领会测试不等量的请求
比如说查看 server status,这种请求,在整个 test suite 中我可能只想跑一次,但是 test data 这种配置没法做到这点
总体来说,数据文件这里的实现是需要小心的,如果可以更细致地控制 collection,那么数据文件的使用还是有它的优点。只是在设计/管理 collection 时,确实是需要小心小心再小心
mock server
postman 本身也提供 mock server 这个功能,之前老版本 mock server 好像是没有隐藏,可以直接从 side bar。现在我用的版本(11.62.5),则是需要手动显示:

然后就可以选择 collection:

然后创建一个 mock server 了:

postman 的 mock server 是直接在 https 上运行------它们相当于运行一个 sandbox environment 直接访问
需要注意的是,mock server 的返回值需要手动保存,官方教程在 Create examples of request responses to illustrate API use cases
整体的流程是这样的:
-
新建一个 example
方法一是直接调用 api,然后将其保存成 example:
或者直接在 sidebar 右键,选择
add example
: -
向 mock server 发出请求
除了 example 这个需要手动写之外,mock server 其他部分算是比较 0 config 了
postman 这里本身应该也是可以通过修改一些参数获取不同的 response 的,不过这块我没多看......毕竟本身有一个 mock server 了
上传文件
从 UI 操作还是比较简单的,这里有两种方法,但是操作入口都是一样的:


通过 newman 上传文件
这里同样可以通过 form data 和 binary 两种方式,前者的话,还需要将 header 修改成 Content-Type: multipart/form-data
newman 中可以把文件名称设置成变量,语法为 {``{file}}
,随后文件名可以:
- 在 terminal,用
--env-var "file=/path/to/test.png"
的方式传入 - 在数据文件,直接在 JSON/CSV 中定义好
file
变量
其他需要注意的是,在 CI 中需要确认 newman 所在的环境,可以直接访问文件所在的路径。这种具体到细节就是 proxy、绝对路径、相对路径、访问权限这些,需要根据具体情况去判断了
官方文档上的 demo 为:
json
{
"info": {
"name": "file-upload"
},
"item": [
{
"request": {
"url": "https://postman-echo.com/post",
"method": "POST",
"body": {
"mode": "formdata",
"formdata": [
{
"key": "file",
"type": "file",
"enabled": true,
"src": "sample-file.txt"
}
]
}
}
}
]
}
结构为:
bash
$ ls
file-upload.postman_collection.json sample-file.txt
$ newman run file-upload.postman_collection.json
eval hacks
其实就是把方法保存在一个环境变量中,然后通过 eval
的方法去调用,当然,这个方法是不太推荐的,毕竟 eval
确实是有安全隐患
以检查 200 code 为例,大体操作是:
jsx
pm.environment.set(
"checkStatus200",
`
function checkStatus200() {
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
}
`
);
随后在其他地方调用:
jsx
let fnCode = pm.environment.get("checkStatus200");
eval(fnCode);
checkStatus200();
脚本内发送请求
即 pm.sendRequest()
简单的使用方法为:
jsx
pm.sendRequest(
"https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js",
(err, res) => {
if (err) {
console.error("Failed to load PapaParse:", err);
return;
}
pm.environment.set("PapaParseLib", res.text());
}
);
CSV 处理
使用 eval
这个就承接发送请求的部分了,上文已经通过 sendRequest
获取了对应的 lib,并保存到变量中,下一步就可以使用 eval 去 parse 并处理,如:
jsx
let papaCode = pm.environment.get("PapaParseLib");
if (papaCode) {
eval(papaCode); // 加载 PapaParse 到当前作用域
}
let csvData = `
username,password
alice,123
bob,456
`;
let parsed = Papa.parse(csvData, { header: true });
console.log("Parsed CSV:", parsed.data);
pm.environment.set("csvUsers", JSON.stringify(parsed.data));
内置支持(非常有限)
postman 内置一些对 csv 的简单支持,大多数都是数据文件的支持,即可以使用 csv 代替 json
除此之外,postman 内部只能简单地借助 lodash 处理/转换字符串,然后处理一些比较简单的 csv------如果格式固定,不需要支持多种 separator
newman 补充
get to now the run, on script etc
newman 本身是一个 npm package,它们 team 也将 newman export 成了一个模块,换句话说,newman 可以作为一个模块导入到 javascript 文件中去。这也就意味着,newman 作为 node module,其实使用方法可以更多一些,相对也可以更加的半自动
比如说官方文档中提供了这样的用法:
jsx
const newman = require("newman"); // require newman in your project
// call newman.run to pass `options` object and wait for callback
newman.run(
{
collection: require("./sample-collection.json"),
reporters: "cli",
},
function (err) {
if (err) {
throw err;
}
console.log("collection run complete!");
}
);
除此之外,newman 也可以手动添加更多的参数,如:
jsx
newman
.run({
collection: require("./sample-collection.json"),
iterationData: [{ var: "data", var_beta: "other_val" }],
globals: {
id: "5bfde907-2a1e-8c5a-2246-4aff74b74236",
name: "test-env",
values: [
{
key: "alpha",
value: "beta",
type: "text",
enabled: true,
},
],
timestamp: 1404119927461,
_postman_variable_scope: "globals",
_postman_exported_at: "2016-10-17T14:31:26.200Z",
_postman_exported_using: "Postman/4.8.0",
},
globalVar: [
{ key: "glboalSecret", value: "globalSecretValue" },
{
key: "globalAnotherSecret",
value: `${process.env.GLOBAL_ANOTHER_SECRET}`,
},
],
environment: {
id: "4454509f-00c3-fd32-d56c-ac1537f31415",
name: "test-env",
values: [
{
key: "foo",
value: "bar",
type: "text",
enabled: true,
},
],
timestamp: 1404119927461,
_postman_variable_scope: "environment",
_postman_exported_at: "2016-10-17T14:26:34.940Z",
_postman_exported_using: "Postman/4.8.0",
},
envVar: [
{ key: "secret", value: "secretValue" },
{ key: "anotherSecret", value: `${process.env.ANOTHER_SECRET}` },
],
})
.on("start", function (err, args) {
// on start of run, log to console
console.log("running a collection...");
})
.on("done", function (err, summary) {
if (err || summary.error) {
console.error("collection run encountered an error.");
} else {
console.log("collection run completed.");
}
});
这个案例中,除了使用 global 和 environment env 设置之外,newman 还能通过参数 globalVar
和 envVar
去 override 一些参数,搭配 node 本身就可以使用 request 去调用 API,安全系数相对而言可以高不少
除此之外,newman 还支持多个生命周期:
Event | Description |
---|---|
start | The start of a collection run |
beforeIteration | Before an iteration commences |
beforeItem | Before an item execution begins (the set of prerequest->request->test) |
beforePrerequest | Before prerequest script is execution starts |
prerequest | After prerequest script execution completes |
beforeRequest | Before an HTTP request is sent |
request | After response of the request is received |
beforeTest | Before test script is execution starts |
test | After test script execution completes |
beforeScript | Before any script (of type test or prerequest ) is executed |
script | After any script (of type test or prerequest ) is executed |
item | When an item (the whole set of prerequest->request->test) completes |
iteration | After an iteration completes |
assertion | This event is triggered for every test assertion done within test scripts |
console | Every time a console function is called from within any script, this event is propagated |
exception | When any asynchronous error happen in scripts this event is triggered |
beforeDone | An event that is triggered prior to the completion of the run |
done | This event is emitted when a collection run has completed, with or without errors |
验证 json schema
这一块 postman 的支持相对而言比 csv 好多了,主要有两个 lib 可以使用 tv4 和 ajv
根据一个 stack overflow:**Postman Schema Validation using TV4** 的回复,说 tv4 已经不太支持,最好是不要继续用了,同样的 post 中也留了 ajv 的使用方法:
jsx
var jsonData = {
categories: [
{
aStringOne: "31000",
aStringTwo: "Yarp",
aStringThree: "More Yarp Indeed",
},
],
};
var Ajv = require("ajv"),
ajv = new Ajv({ logger: console, allErrors: true }),
schema = {
type: "object",
required: ["categories"],
properties: {
categories: {
type: "array",
items: {
type: "object",
required: ["aStringOne", "aStringTwo", "aStringThree"],
properties: {
aStringOne: { type: "string" },
aStringTwo: { type: "integer" },
aStringThree: { type: "boolean" },
},
},
},
},
};
pm.test("Schema is valid", function () {
pm.expect(
ajv.validate(schema, jsonData),
JSON.stringify(ajv.errors)
).to.be.true;
});
修改 view
postman 还有一个 2 col view,这个 view 在看 params/结果/测试的时候比较方便:

总结
整体上,Postman 的价值还是要分两个层面来看
浅层的功能,也就是第一篇笔记可以覆盖的内容,如 CRUD、结果验证,这些上手快、也确实好用。但问题在于,这类功能替代品很多,Insomnia、Bruno 之类都能做到,甚至更轻量。而且 Postman 很多功能都是 Postman 的 Cloud ,也就是说数据会被留在 Postman 的平台中,这就会带来安全隐患。在一些对安全要求很高的场景(像金融、银行业),已经有不少企业在 infra 层禁用 Postman------虽然可以访问官方,但是无法登陆和下载 Postman client 和插件
深层的部分,更多取决于业务需求。如果是非开发角色,基于已有的 collections,只是想快速上传 CSV 文件,跑一些 smoke test,验证 API 有没有大问题,再把 endpoints 拆分进不同的 collection,那么 Postman 在这种情况下还是能提供一些便利的
但如果是开发或者 QA,要做更复杂的验证和端到端测试,Postman 就显得力不从心了。更适合的工具可能是 Cypress / Playwright,灵活性和可维护性都会更高