开发技巧

给一个待办事项 API 写一套完整的 Postman 自动化测试:登录拿令牌、新建、修改、查询、删除,外加空标题、未登录等异常情况

给一个待办事项 API 写一套完整的 Postman 自动化测试:登录拿令牌、新建、修改、查询、删除,外加空标题、未登录等异常情况。8 个请求按顺序执行,前一个请求的结果自动传给下一个,点一次 Run 就知道接口有没有坏。

BINBHO · 图文教程

给一个待办事项 API 写一套完整的 Postman 自动化测试:登录拿令牌、新建、修改、查询、删除,外加空标题、未登录等异常情况。8 个请求按顺序执行,前一个请求的结果自动传给下一个,点一次 Run 就知道接口有没有坏。

先看清成品

成品是一个 Postman 集合加一个环境。本文已对本地练习 API 实际运行:Newman 6.2.3 和 Postman CLI 1.75.0 都执行了 8 个请求、13 条断言,全部通过。在 Postman 桌面应用里手动操作的部分没有截图验证,菜单名称按官方文档编写。

注意:只对你自己的或者明确允许测试的 API 运行自动化测试,不要对别人的线上服务做压力测试或扫描。

做完之后你会得到什么?

成果 说明
练习 API server.js,登录 + 待办增删改查
测试集合 8 个请求,覆盖正常和异常
自动传值 令牌和新建的 ID 存进集合变量
结构校验 用 JSON Schema 检查返回格式
命令行运行 Newman 或 Postman CLI

需要准备哪些环境?

项目 要求 说明
Postman 桌面应用 免费账号即可 官网下载,各计划限制以价格页为准
Node.js 18 或更高 运行练习 API,本文使用 22.23.3
练习 API 本文提供的 server.js 不依赖任何第三方包
Newman npm i -g newman 可选,本文使用 6.2.3

第一步:怎样启动练习 API?

新建文件夹,放入 package.json(内容 {"type":"module","private":true})和下面的 server.js:

// 练习用的待办 API:登录拿令牌,增删改查待办。数据在内存里,重启清空。
// 运行:node server.js   监听 http://localhost:3000
import http from 'node:http';
import crypto from 'node:crypto';

const USERS = { alice: 'pass123', bob: 'hunter2' };
const tokens = new Map(); // token -> { user, exp }
let todos = []; let nextId = 1;
const TTL = Number(process.env.TOKEN_TTL_MS || 60 * 60 * 1000);

const send = (res, code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(obj === undefined ? '' : JSON.stringify(obj)); };
const body = (req) => new Promise((ok) => { let d = ''; req.on('data', (c) => d += c); req.on('end', () => { try { ok(d ? JSON.parse(d) : {}); } catch { ok(null); } }); });

export const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://x');
  if (req.method === 'POST' && url.pathname === '/login') {
    const b = await body(req);
    if (!b || USERS[b.username] !== b.password) return send(res, 401, { error: '用户名或密码错误' });
    const token = crypto.randomBytes(16).toString('hex');
    tokens.set(token, { user: b.username, exp: Date.now() + TTL });
    return send(res, 200, { token, expires_in: TTL / 1000 });
  }
  const auth = (req.headers.authorization || '').replace(/^Bearer /, '');
  const t = tokens.get(auth);
  if (!t || t.exp < Date.now()) return send(res, 401, { error: '未登录或令牌过期' });

  const m = url.pathname.match(/^\/todos(?:\/(\d+))?$/);
  if (!m) return send(res, 404, { error: '没有这个接口' });
  const id = m[1] && Number(m[1]);
  const mine = todos.filter((x) => x.owner === t.user);
  if (req.method === 'GET' && !id) return send(res, 200, mine);
  if (req.method === 'POST' && !id) {
    const b = await body(req);
    if (!b || typeof b.title !== 'string' || !b.title.trim()) return send(res, 400, { error: 'title 必填' });
    if (b.title.length > 100) return send(res, 400, { error: 'title 最多 100 字' });
    const item = { id: nextId++, title: b.title.trim(), done: false, owner: t.user };
    todos.push(item);
    return send(res, 201, item);
  }
  const item = mine.find((x) => x.id === id);
  if (!item) return send(res, 404, { error: '待办不存在' });
  if (req.method === 'GET') return send(res, 200, item);
  if (req.method === 'PATCH') { const b = await body(req); if (typeof b?.done === 'boolean') item.done = b.done; return send(res, 200, item); }
  if (req.method === 'DELETE') { todos = todos.filter((x) => x !== item); return send(res, 204); }
  send(res, 405, { error: '方法不支持' });
});

if (import.meta.url === `file://${process.argv[1]}`) server.listen(process.env.PORT || 3000, () => console.log('API 已启动 http://localhost:' + (process.env.PORT || 3000)));
node server.js

看到 API 已启动 http://localhost:3000 即可。用 curl 试一下:

curl -X POST localhost:3000/login -d '{"username":"alice","password":"pass123"}'

期望返回 {"token":"…32 位…","expires_in":3600}。

第二步:怎样在 Postman 里建集合?

1. 建环境

左侧 Environments → +,名称 local,添加三个变量:

变量 值 类型
baseUrl http://localhost:3000 default
username alice default
password pass123 secret

右上角环境下拉框选中 local。

2. 建集合并设置统一鉴权

新建集合“待办 API 测试”,在集合的 Authorization 标签选 Bearer Token,Token 填 {{token}}。之后集合里的请求默认继承这个设置。

在集合的 Variables 标签添加 token、todoId 两个空变量。

3. 逐个添加请求和测试脚本

每个请求的脚本写在 Scripts → Post-response。带请求体的选 Body → raw → JSON。

1 登录

POST {{baseUrl}}/login,请求体 {"username": "{{username}}", "password": "{{password}}"} ,Authorization 选 No Auth

pm.test('状态码 200', () => pm.response.to.have.status(200));
const j = pm.response.json();
pm.test('返回 token', () => pm.expect(j.token).to.be.a('string').and.have.lengthOf(32));
pm.collectionVariables.set('token', j.token);

2 新建待办

POST {{baseUrl}}/todos,请求体 {"title": "买牛奶 {{$timestamp}}"}

pm.test('状态码 201', () => pm.response.to.have.status(201));
const j = pm.response.json();
pm.test('done 默认为 false', () => pm.expect(j.done).to.eql(false));
pm.test('响应时间小于 500ms', () => pm.expect(pm.response.responseTime).to.be.below(500));
pm.collectionVariables.set('todoId', j.id);

3 标记完成

PATCH {{baseUrl}}/todos/{{todoId}},请求体 {"done": true}

pm.test('done 变为 true', () => pm.expect(pm.response.json().done).to.eql(true));

4 列表包含新待办

GET {{baseUrl}}/todos

const id = Number(pm.collectionVariables.get('todoId'));
pm.test('列表里能找到', () => pm.expect(pm.response.json().map(t => t.id)).to.include(id));
pm.test('符合结构', () => pm.response.to.have.jsonSchema({
  type: 'array',
  items: { type: 'object', required: ['id', 'title', 'done'], properties: { id: { type: 'integer' }, title: { type: 'string' }, done: { type: 'boolean' } } }
}));

5 空标题应被拒绝

POST {{baseUrl}}/todos,请求体 {"title": ""}

pm.test('状态码 400', () => pm.response.to.have.status(400));
pm.test('有错误信息', () => pm.expect(pm.response.json().error).to.include('title'));

6 删除

DELETE {{baseUrl}}/todos/{{todoId}}

pm.test('状态码 204', () => pm.response.to.have.status(204));

7 删除后查询应 404

GET {{baseUrl}}/todos/{{todoId}}

pm.test('状态码 404', () => pm.response.to.have.status(404));

8 不带令牌应 401

GET {{baseUrl}}/todos ,Authorization 选 No Auth

pm.test('状态码 401', () => pm.response.to.have.status(401));
用法 作用
pm.test(名称, 函数) 定义一条断言
pm.response.to.have.status() 检查状态码
pm.collectionVariables.set() 把值存到集合变量,后面的请求用 {{变量}} 引用
{{$timestamp}} 内置动态变量,让每次标题不同
jsonSchema 检查返回结构

懒得手动建的话,本文的 -src 文件夹里有 todo-api.postman_collection.json 和 local.postman_environment.json,在 Postman 里点 Import 导入即可。

第三步:怎样运行?

1. 在应用里运行

集合右侧 ⋯ → Run collection,保持顺序,点 Run。期望 8 个请求、13 条断言全部绿色。

2. 用命令行运行

npm i -g newman
newman run todo-api.postman_collection.json -e local.postman_environment.json

本文实际输出末尾:

│                requests │               8 │               0 │
│            test-scripts │               8 │               0 │
│              assertions │              13 │               0 │

也可以用 Postman 官方的 Postman CLI:

postman collection run todo-api.postman_collection.json -e local.postman_environment.json

本文实测未登录时也能运行本地集合文件,结束时会提示需要 postman login 才能把结果上传到 Postman 云端。

出问题时该怎么调?

1. 打开 Postman Console

底部 Console 能看到每个请求的完整请求头和响应,变量是否替换成功一看便知。脚本里也可以用 console.log()。

2. 第 2 个请求开始全部 401

原因 解决方法
登录请求没把令牌存进去 检查第 1 个请求的脚本
集合鉴权没设置 集合 Authorization 选 Bearer Token
请求自己设置了 No Auth 改回 Inherit auth from parent

3. ECONNREFUSED

练习 API 没启动,或端口不是 3000。

常见问题怎么处理?

现象 原因 解决方法
{{todoId}} 原样发出去 变量没设置 必须按顺序从第 1 个跑
单独运行第 7 个失败 依赖前面的请求 整个集合一起跑
中文测试名乱码 终端编码 用 UTF-8 终端
响应时间断言偶尔失败 机器繁忙 适当放宽阈值
重启 API 后数据没了 练习 API 存在内存里 正常现象

项目资源

用途 地址
Postman 下载 https://www.postman.com/downloads/
测试脚本写法 https://learning.postman.com/docs/tests-and-scripts/write-scripts/test-scripts/
脚本 API 参考(pm 对象) https://learning.postman.com/docs/tests-and-scripts/write-scripts/postman-sandbox-reference/overview/
变量 https://learning.postman.com/docs/sending-requests/variables/variables/
运行集合(Postman CLI) https://learning.postman.com/docs/postman-cli/postman-cli-run-collection/
Newman https://github.com/postmanlabs/newman
导入数据 https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/

建议先导入现成的集合跑通一次,再自己从头建一遍,熟悉变量传递和断言写法。

然后把同样的思路用到你自己项目的接口上,先覆盖登录和最核心的两三个接口。