
开篇
上一篇文章,我们为 task-api 确定了接口契约和统一错误结构。
今天给这个项目补测试,但重点不是让 AI 凑出更多测试文件,而是让它回答一个更重要的问题:
哪些场景最可能让这组 API 出错?
本篇使用 Vitest + Supertest,测试对象是 Express 应用本身,不启动真实端口。流程如下:
text
业务规则
↓
测试场景
↓
接口测试
↓
发现隐藏 Bug
↓
修复并回归
一、先让 AI 设计场景,不要直接生成代码
我们已经确定了任务 API 的基本规则:
title必须是非空字符串。- 创建成功返回
201。 PATCH只允许修改completed。- 任务不存在返回
404和TASK_NOT_FOUND。 - 删除成功返回
204。
把这些规则交给 AI:
text
请根据下面的 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 写代码:
text
请根据已确认的测试场景生成 task-api 的接口测试。
技术要求:
- 使用 Vitest 和 Supertest。
- 从 src/app.js 导入 app,不监听真实端口。
- 每个测试开始前重置内存数据。
- 每个测试只验证一个主要行为,但可以断言必要的副作用。
- 同时断言 HTTP 状态码、错误 code 和关键响应字段。
- 不要修改业务代码来迁就测试。
请先输出测试文件路径和测试分组,再输出完整代码。
最后说明每个测试对应哪条业务规则。
这里的"不要修改业务代码来迁就测试"很重要。
测试失败时,先判断是测试假设错了,还是业务代码真的有问题。不能为了让测试变绿,就把断言改成一个没有意义的宽松判断。
四、一段有价值的测试示例
以"PATCH 不能修改 id"为例:
javascript
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);
});
如果原代码直接把请求体合并到任务对象:
javascript
Object.assign(task, req.body);
这个测试就可能失败,并暴露出一个隐藏 Bug:客户端可以覆盖服务端生成的 id。
正确做法是只读取允许修改的字段:
javascript
task.completed = req.body.completed;
测试的价值就在这里:它不是重复实现代码,而是证明一个重要边界没有被破坏。
五、再补一个经常被忽略的删除测试
删除接口不能只断言 204:
javascript
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 个问题审查测试:
text
[ ] 测试是否来自明确的业务规则?
[ ] 失败时,能否说明具体行为不符合预期?
[ ] 是否检查了响应之外的数据变化?
[ ] 是否覆盖了空值、非法值、资源不存在等边界?
[ ] 测试之间是否相互独立?
还要警惕这几种"看起来很努力"的测试:
- 只断言响应不为空。
- 只断言函数被调用一次。
- 只测试永远不会失败的固定输入。
- 大量 Mock,却没有验证真实数据是否改变。
- 为了覆盖率,给每一行代码机械地写测试。
测试数量多,不等于风险覆盖得好。
七、运行测试并记录结果
在 package.json 中配置:
json
{
"scripts": {
"test": "vitest run"
}
}
执行:
bash
npm test
建议把失败结果记录成问题,而不是直接删掉失败用例:
text
问题:PATCH 可以覆盖任务 id
触发:发送 { "id": 999, "completed": true }
影响:后续查询和删除可能操作错误资源
修复:只允许更新 completed 字段
回归:重新运行 PATCH 边界测试
这份记录也可以交给 AI,让它继续分析修复是否完整。
总结
用 AI 设计测试用例,推荐遵循下面的顺序:
text
先给业务规则
↓
让 AI 列测试场景
↓
人工确认边界
↓
生成接口测试
↓
根据失败结果修复
↓
重新执行回归
- 测试应该验证业务风险,而不是单纯追求覆盖率。
- 正常流程、参数边界、资源不存在和数据副作用都要考虑。
- 一个好的测试,应该能够发现真实 Bug。
- 测试失败是反馈,不要为了变绿而削弱断言。
下一篇文章,我们将完成这个小项目的最后一轮工程检查:
《小项目实战 4:代码审查、重构与提交前检查》
✍坚持原创,求关注,点赞,收藏