Postman 学习笔记 IV:Workflow、Newman 与 Mock Server 实战技巧

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.requestIdpm.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.setpm.global.get 进行跨请求合作。如下面的 3 个请求的测试:

  • Request A

    jsx 复制代码
    const data = pm.response.json().items;
    pm.globals.set("itemsList", JSON.stringify(data));
    pm.globals.set("loopIndex", "0");
    
    pm.execution.setNextRequest("Request B");
  • Request B

    jsx 复制代码
    let 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

    jsx 复制代码
    pm.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 中获取数据:

    jsx 复制代码
    if (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

整体的流程是这样的:

  1. 新建一个 example

    方法一是直接调用 api,然后将其保存成 example:

    或者直接在 sidebar 右键,选择 add example

  2. 向 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 还能通过参数 globalVarenvVar 去 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,灵活性和可维护性都会更高

相关推荐
知识分享小能手3 小时前
微信小程序入门学习教程,从入门到精通,微信小程序常用API(下)——知识点详解 + 案例实战(5)
前端·javascript·学习·微信小程序·小程序·vue·前端开发
编程攻城狮4 小时前
第 5 天:C 语言运算符与表达式 —— 数据处理的工具集
c语言·开发语言·学习
WPG大大通5 小时前
从数据到模型:Label Studio 开源标注工具完整实施指南
经验分享·笔记·ai·系统架构·开源·大大通
-一杯为品-5 小时前
【强化学习】#8 DQN(深度Q学习)
学习
charlie1145141916 小时前
精读C++20设计模式——结构型设计模式:享元模式
c++·笔记·学习·设计模式·享元模式·c++20
EQ-雪梨蛋花汤6 小时前
【Unity笔记】Unity XR 模式下 Point Light 不生效的原因与解决方法
笔记·unity·xr
qianmo20217 小时前
基于deepseek学习三角函数相关
学习·算法
序属秋秋秋9 小时前
《C++进阶之C++11》【lambda表达式 + 包装器】
c++·笔记·学习·c++11·lambda表达式·包装器
Hello_Embed9 小时前
STM32 智能垃圾桶项目笔记(四):PWM 回顾与舵机(SG90)控制实现
笔记·stm32·单片机·学习·嵌入式软件