小项目实战 3:用 AI 设计测试用例并发现隐藏 Bug

文章来源声明: 原文作者:全栈弄潮儿; 来源站点:掘金; 原文链接:https://juejin.cn/post/7691512357835948078; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

给用 AI 补测试的开发者一份可落地清单:场景先行、断言有据、失败留痕。适合 Node/Express 接口测试与 AI 辅助编码场景。

![在这里插入图片描述](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e6b3b49f838d4b579e8fd722fde96933~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YWo5qCI5byE5r2u5YS_:q75.awebp?rk3s=f64ab15b&x-expires=1791542750&x-signature=QqSG%2F%2FQgUzOPgdXPTlv%2FpkDS1p0%3D)

开篇

上一篇文章,我们为 task-api 确定了接口契约和统一错误结构。

今天给这个项目补测试,但重点不是让 AI 凑出更多测试文件,而是让它回答一个更重要的问题:

哪些场景最可能让这组 API 出错?

本篇使用 Vitest + Supertest,测试对象是 Express 应用本身,不启动真实端口。流程如下:

业务规则
  ↓
测试场景
  ↓
接口测试
  ↓
发现隐藏 Bug
  ↓
修复并回归

一、先让 AI 设计场景,不要直接生成代码

我们已经确定了任务 API 的基本规则:

  • title 必须是非空字符串。
  • 创建成功返回 201。
  • PATCH 只允许修改 completed。
  • 任务不存在返回 404 和 TASK_NOT_FOUND。
  • 删除成功返回 204。

把这些规则交给 AI:

请根据下面的 task-api 接口契约设计测试场景,先不要生成测试代码。

接口:
- GET /api/tasks
- POST /api/tasks
- PATCH /api/tasks/:id
- DELETE /api/tasks/:id

规则:
- title 必须是非空字符串。
- POST 成功返回 201。
- PATCH 只允许修改 completed,且 completed 必须是布尔值。
- 任务不存在返回 404 和 TASK_NOT_FOUND。
- DELETE 成功返回 204 且没有响应体。

请按以下分类输出:
1. 正常流程。
2. 参数边界。
3. 资源不存在。
4. 非法字段和状态转换。
5. 数据副作用。

每个场景写清楚:请求、预期状态码、预期响应、需要验证的数据变化。
不要把“代码执行到了”当成测试目标。

先看场景表有两个好处:

  • 可以发现需求中还没有定义的行为。
  • 可以避免 AI 只测试正常请求和 null 参数。

二、测试场景应该覆盖什么

一组够用的最小场景如下:

分类场景重点断言
正常创建并查询任务状态码、字段和列表数据
参数`title` 为空或不是字符串返回 400 和 `INVALID_TITLE`
修改`completed` 不是布尔值返回 400 和 `INVALID_STATUS`
资源修改或删除不存在的任务返回 404
字段PATCH 传入 `id``id` 不被覆盖
删除删除后再次查询任务确实不存在
隔离每个测试使用独立数据测试之间互不影响

最后两项很容易被忽略。

如果测试只断言响应状态码,不检查数据是否真的改变,删除接口即使什么都没删也可能通过测试。

如果测试共用一份内存数据,前一个测试创建的任务可能影响后一个测试,测试结果就不可信。

三、让 AI 生成接口测试

场景确认后,再让 AI 写代码:

请根据已确认的测试场景生成 task-api 的接口测试。

技术要求:
- 使用 Vitest 和 Supertest。
- 从 src/app.js 导入 app,不监听真实端口。
- 每个测试开始前重置内存数据。
- 每个测试只验证一个主要行为,但可以断言必要的副作用。
- 同时断言 HTTP 状态码、错误 code 和关键响应字段。
- 不要修改业务代码来迁就测试。

请先输出测试文件路径和测试分组,再输出完整代码。
最后说明每个测试对应哪条业务规则。

这里的“不要修改业务代码来迁就测试”很重要。

测试失败时,先判断是测试假设错了,还是业务代码真的有问题。不能为了让测试变绿,就把断言改成一个没有意义的宽松判断。

四、一段有价值的测试示例

以“PATCH 不能修改 id”为例:

it("不允许通过 PATCH 修改任务 id", async () => {
  const created = await request(app)
    .post("/api/tasks")
    .send({ title: "原始任务" });

  const taskId = created.body.id;

  const response = await request(app)
    .patch(`/api/tasks/${taskId}`)
    .send({ id: 999, completed: true });

  expect(response.status).toBe(200);
  expect(response.body.id).toBe(taskId);
  expect(response.body.completed).toBe(true);
});

如果原代码直接把请求体合并到任务对象:

Object.assign(task, req.body);

这个测试就可能失败,并暴露出一个隐藏 Bug:客户端可以覆盖服务端生成的 id。

正确做法是只读取允许修改的字段:

task.completed = req.body.completed;

测试的价值就在这里:它不是重复实现代码,而是证明一个重要边界没有被破坏。

五、再补一个经常被忽略的删除测试

删除接口不能只断言 204:

it("删除任务后,任务不再存在", async () => {
  const created = await request(app)
    .post("/api/tasks")
    .send({ title: "待删除任务" });

  const taskId = created.body.id;

  const deleted = await request(app)
    .delete(`/api/tasks/${taskId}`);

  expect(deleted.status).toBe(204);
  expect(deleted.text).toBe("");

  const query = await request(app).get(`/api/tasks/${taskId}`);
  expect(query.status).toBe(404);
  expect(query.body.error.code).toBe("TASK_NOT_FOUND");
});

如果项目当前没有 GET /api/tasks/:id,可以改为查询列表,再确认返回数组中不包含该任务。

测试应该适配已经确认的接口契约,不能为了示例偷偷增加接口。

六、如何判断 AI 生成的测试有没有价值

我会用下面 5 个问题审查测试:

[ ] 测试是否来自明确的业务规则?
[ ] 失败时,能否说明具体行为不符合预期?
[ ] 是否检查了响应之外的数据变化?
[ ] 是否覆盖了空值、非法值、资源不存在等边界?
[ ] 测试之间是否相互独立?

还要警惕这几种“看起来很努力”的测试:

  • 只断言响应不为空。
  • 只断言函数被调用一次。
  • 只测试永远不会失败的固定输入。
  • 大量 Mock,却没有验证真实数据是否改变。
  • 为了覆盖率,给每一行代码机械地写测试。

测试数量多,不等于风险覆盖得好。

七、运行测试并记录结果

在 package.json 中配置:

{
  "scripts": {
    "test": "vitest run"
  }
}

执行:

npm test

建议把失败结果记录成问题,而不是直接删掉失败用例:

问题:PATCH 可以覆盖任务 id
触发:发送 { "id": 999, "completed": true }
影响:后续查询和删除可能操作错误资源
修复:只允许更新 completed 字段
回归:重新运行 PATCH 边界测试

这份记录也可以交给 AI,让它继续分析修复是否完整。

总结

用 AI 设计测试用例,推荐遵循下面的顺序:

先给业务规则
  ↓
让 AI 列测试场景
  ↓
人工确认边界
  ↓
生成接口测试
  ↓
根据失败结果修复
  ↓
重新执行回归

  • 测试应该验证业务风险,而不是单纯追求覆盖率。
  • 正常流程、参数边界、资源不存在和数据副作用都要考虑。
  • 一个好的测试,应该能够发现真实 Bug。
  • 测试失败是反馈,不要为了变绿而削弱断言。

下一篇文章,我们将完成这个小项目的最后一轮工程检查:

《小项目实战 4:代码审查、重构与提交前检查》


✍坚持原创,求关注,点赞,收藏