用 AI 生成接口测试用例,别只测成功场景
很多后端接口上线前,测试看起来是有的。
比如创建订单接口,有一条测试:
test("should create order successfully", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.send({
productId: "p1001",
quantity: 1
});
expect(res.status).toBe(201);
});
跑一下,绿了。
然后大家就放心了。
但线上问题往往不是出在“正常用户正常提交正常参数”这种场景里。
更常见的问题是:
用户没登录怎么办?
quantity 传 0 怎么办?
productId 为空怎么办?
商品不存在怎么办?
库存不足怎么办?
重复点击提交按钮怎么办?
优惠券失效怎么办?
接口返回结构和前端约定不一致怎么办?
异常场景 code 字段是不是统一?
测试数据会不会污染下一条用例?
所以,用 AI 生成接口测试用例,最怕的是只让它写“成功场景”。
真正应该让 AI 做的,是先拆测试矩阵,再生成测试代码。

#一、接口测试不要从代码开始,要从场景开始
很多人会这样问 AI:
帮我给这个接口写 Jest 测试。
AI 通常会给你几条测试,能跑,但不一定完整。
更好的问法应该是:
你先不要写测试代码。
请根据这个接口设计,先生成测试场景矩阵。
接口:POST /api/orders
功能:创建订单
鉴权:需要登录
入参:productId、quantity、couponCode
幂等:需要 idempotency-key
统一返回格式:{ code, message, data }
请按以下维度输出:
1. 正常场景
2. 参数缺失
3. 参数类型错误
4. 参数边界值
5. 鉴权失败
6. 资源不存在
7. 业务状态异常
8. 重复提交
9. 错误码一致性
10. 返回结构一致性
先输出 Markdown 表格,不要写代码。
这一步看起来多余,但它能把测试从“几条 case”变成“完整覆盖思路”。
测试代码只是结果,测试矩阵才是设计。
#二、一张测试矩阵应该长什么样?
以创建订单接口为例,AI 生成的测试矩阵可以是这样:
| 场景分类 | 测试点 | 输入条件 | 预期结果 | 优先级 |
|---|---|---|---|---|
| 正常场景 | 创建订单成功 | 登录用户、有效 productId、quantity=1 | 201,返回 orderId | P0 |
| 鉴权失败 | 未登录创建订单 | 缺少用户身份 | 401,code=UNAUTHORIZED | P0 |
| 参数缺失 | 缺少 productId | body 无 productId | 400,code=VALIDATION_ERROR | P0 |
| 参数缺失 | 缺少 quantity | body 无 quantity | 400,code=VALIDATION_ERROR | P0 |
| 参数边界 | quantity=0 | 数量为 0 | 400,code=VALIDATION_ERROR | P0 |
| 参数边界 | quantity=-1 | 数量为负数 | 400,code=VALIDATION_ERROR | P0 |
| 资源不存在 | 商品不存在 | productId 不存在 | 404,code=PRODUCT_NOT_FOUND | P0 |
| 业务异常 | 库存不足 | quantity 大于库存 | 409,code=ORDER_STOCK_NOT_ENOUGH | P0 |
| 幂等处理 | 重复提交 | 相同 idempotency-key 提交两次 | 第二次 409 | P0 |
| 返回结构 | 所有失败响应 | 检查 code/message/data | 结构一致 | P1 |
| 文档一致 | 响应字段 | orderId、orderNo、status、payAmount | 字段齐全 | P1 |
有了这张表,再让 AI 写测试,质量会明显高很多。
因为你不是让它“随便想几个用例”,而是让它按工程维度补齐。
#三、准备一个最小 Express 接口示例
下面用 Node.js + Express + Jest + Supertest 演示。
项目结构:
api-test-demo/
├── package.json
├── src/
│ ├── app.js
│ ├── controllers/
│ │ └── orderController.js
│ ├── middleware/
│ │ └── auth.js
│ ├── routes/
│ │ └── order.js
│ └── utils/
│ └── response.js
└── tests/
└── order.test.js
##package.json
{
"name": "api-test-demo",
"version": "1.0.0",
"type": "commonjs",
"scripts": {
"start": "node src/app.js",
"test": "jest --runInBand"
},
"dependencies": {
"express": "^4.18.3",
"zod": "^3.23.8"
},
"devDependencies": {
"jest": "^29.7.0",
"supertest": "^6.3.4"
}
}
安装依赖:
npm install
npm test
#四、接口代码:先有可测对象
##src/utils/response.js
function success(data = null, message = "ok") {
return {
code: "SUCCESS",
message,
data
};
}
function fail(code, message, data = null) {
return {
code,
message,
data
};
}
module.exports = {
success,
fail
};
##src/middleware/auth.js
const { fail } = require("../utils/response");
function authRequired(req, res, next) {
const userId = req.headers["x-user-id"];
if (!userId) {
return res.status(401).json(
fail("UNAUTHORIZED", "user is not logged in")
);
}
req.user = {
id: String(userId)
};
next();
}
module.exports = {
authRequired
};
##src/controllers/orderController.js
const { z } = require("zod");
const { success, fail } = require("../utils/response");
const products = [
{
id: "p1001",
name: "Keyboard",
price: 199,
stock: 5
}
];
const usedIdempotencyKeys = new Set();
const createOrderSchema = z.object({
productId: z.string().min(1),
quantity: z.number().int().positive(),
couponCode: z.string().optional()
});
function resetOrderStoreForTest() {
usedIdempotencyKeys.clear();
}
function createOrder(req, res) {
const idempotencyKey = req.headers["idempotency-key"];
if (!idempotencyKey) {
return res.status(400).json(
fail("IDEMPOTENCY_KEY_REQUIRED", "idempotency-key header is required")
);
}
if (usedIdempotencyKeys.has(idempotencyKey)) {
return res.status(409).json(
fail("DUPLICATE_REQUEST", "duplicate request")
);
}
const parsed = createOrderSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(
fail("VALIDATION_ERROR", "invalid request body", parsed.error.flatten())
);
}
const { productId, quantity, couponCode } = parsed.data;
const product = products.find((item) => item.id === productId);
if (!product) {
return res.status(404).json(
fail("PRODUCT_NOT_FOUND", "product not found")
);
}
if (product.stock < quantity) {
return res.status(409).json(
fail("ORDER_STOCK_NOT_ENOUGH", "product stock is not enough")
);
}
if (couponCode && couponCode !== "NEW_USER_10") {
return res.status(400).json(
fail("COUPON_INVALID", "coupon is invalid")
);
}
usedIdempotencyKeys.add(idempotencyKey);
const discount = couponCode === "NEW_USER_10" ? 10 : 0;
return res.status(201).json(
success({
orderId: "o_" + Date.now(),
orderNo: "NO" + Date.now(),
status: "PENDING_PAYMENT",
payAmount: product.price * quantity - discount
})
);
}
module.exports = {
createOrder,
resetOrderStoreForTest
};
##src/routes/order.js
const express = require("express");
const { authRequired } = require("../middleware/auth");
const { createOrder } = require("../controllers/orderController");
const router = express.Router();
router.post("/orders", authRequired, createOrder);
module.exports = router;
##src/app.js
const express = require("express");
const orderRoutes = require("./routes/order");
const app = express();
app.use(express.json());
app.use("/api", orderRoutes);
app.get("/health", (req, res) => {
res.json({
status: "ok"
});
});
if (require.main === module) {
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`server running at http://localhost:${port}`);
});
}
module.exports = app;
这里有一个特意设计的小点:resetOrderStoreForTest()。
因为示例里用内存 Set 存幂等 key,如果不在每条测试前清理,测试之间会互相影响。
真实项目里可能是 Redis、数据库或事务回滚,原理一样:测试要保证隔离。
#五、让 AI 生成测试代码时,要带上约束
不要只说“写测试”。
可以这样写:
请根据下面的测试矩阵,为 POST /api/orders 生成 Jest + Supertest 测试代码。
要求:
1. 每条测试都检查 HTTP 状态码。
2. 每条测试都检查响应 body.code。
3. 每条失败测试都检查返回结构包含 code、message、data。
4. 每条测试使用不同的 idempotency-key。
5. 每个测试前调用 resetOrderStoreForTest,避免测试互相污染。
6. 不依赖真实数据库。
7. 不只测试成功场景。
8. 输出完整 tests/order.test.js。
这个 Prompt 的关键是把测试质量要求说清楚。
AI 生成测试时,不怕它写代码,怕它不知道你要检查什么。
#六、完整测试示例
##tests/order.test.js
const request = require("supertest");
const app = require("../src/app");
const { resetOrderStoreForTest } = require("../src/controllers/orderController");
function expectCommonResponseShape(body) {
expect(body).toHaveProperty("code");
expect(body).toHaveProperty("message");
expect(body).toHaveProperty("data");
}
describe("POST /api/orders", () => {
beforeEach(() => {
resetOrderStoreForTest();
});
test("should create order successfully", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-success")
.send({
productId: "p1001",
quantity: 1,
couponCode: "NEW_USER_10"
});
expect(res.status).toBe(201);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("SUCCESS");
expect(res.body.data.orderId).toBeTruthy();
expect(res.body.data.orderNo).toBeTruthy();
expect(res.body.data.status).toBe("PENDING_PAYMENT");
expect(res.body.data.payAmount).toBe(189);
});
test("should reject unauthenticated request", async () => {
const res = await request(app)
.post("/api/orders")
.set("idempotency-key", "key-unauth")
.send({
productId: "p1001",
quantity: 1
});
expect(res.status).toBe(401);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("UNAUTHORIZED");
});
test("should reject missing idempotency key", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.send({
productId: "p1001",
quantity: 1
});
expect(res.status).toBe(400);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("IDEMPOTENCY_KEY_REQUIRED");
});
test("should reject missing productId", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-missing-product")
.send({
quantity: 1
});
expect(res.status).toBe(400);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("VALIDATION_ERROR");
});
test("should reject missing quantity", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-missing-quantity")
.send({
productId: "p1001"
});
expect(res.status).toBe(400);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("VALIDATION_ERROR");
});
test("should reject quantity equals zero", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-quantity-zero")
.send({
productId: "p1001",
quantity: 0
});
expect(res.status).toBe(400);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("VALIDATION_ERROR");
});
test("should reject product not found", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-product-not-found")
.send({
productId: "not-exists",
quantity: 1
});
expect(res.status).toBe(404);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("PRODUCT_NOT_FOUND");
});
test("should reject stock not enough", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-stock-not-enough")
.send({
productId: "p1001",
quantity: 99
});
expect(res.status).toBe(409);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("ORDER_STOCK_NOT_ENOUGH");
});
test("should reject invalid coupon", async () => {
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-invalid-coupon")
.send({
productId: "p1001",
quantity: 1,
couponCode: "BAD_COUPON"
});
expect(res.status).toBe(400);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("COUPON_INVALID");
});
test("should reject duplicate request with same idempotency key", async () => {
const payload = {
productId: "p1001",
quantity: 1
};
await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-duplicate")
.send(payload);
const res = await request(app)
.post("/api/orders")
.set("x-user-id", "u1001")
.set("idempotency-key", "key-duplicate")
.send(payload);
expect(res.status).toBe(409);
expectCommonResponseShape(res.body);
expect(res.body.code).toBe("DUPLICATE_REQUEST");
});
});
这组测试比单纯测成功场景更接近真实项目需要。
它覆盖了:
登录态;
幂等 key;
参数缺失;
参数边界;
资源不存在;
库存不足;
优惠券异常;
重复提交;
统一返回结构。
#七、AI 生成测试后,还要人工检查什么?
AI 写出来的测试不能直接当作最终版本。
至少要检查 8 个点:
| 检查项 | 要看什么 |
|---|---|
| 是否只测成功场景 | 如果只有 happy path,覆盖不够 |
| 断言是否太弱 | 只检查 status 不够,还要检查 code 和字段 |
| 测试是否隔离 | 用例之间不能互相污染 |
| 测试数据是否稳定 | 不要依赖当前时间、随机数据导致不稳定 |
| 错误码是否准确 | 要和接口设计表一致 |
| 返回结构是否统一 | code/message/data 是否每个场景都有 |
| 是否覆盖边界值 | 0、负数、空字符串、缺字段都要测 |
| 是否适合生产逻辑 | 示例里的内存数据要替换成数据库或 mock |
一个常见问题是:AI 会生成“看起来很多”的测试,但断言很弱。
比如:
expect(res.status).toBeTruthy();
这种断言没有什么价值。
更好的写法是:
expect(res.status).toBe(409);
expect(res.body.code).toBe("ORDER_STOCK_NOT_ENOUGH");
expect(res.body.data).toBeNull();
测试不是为了让覆盖率好看,而是为了让问题更早暴露。
#八、测试矩阵也可以沉淀成团队模板
如果团队经常用 AI 写接口测试,可以把测试矩阵做成固定模板。
请为以下接口生成测试矩阵,不要直接写代码。
接口信息:
- 名称:
- 路径:
- 方法:
- 鉴权:
- 请求参数:
- 返回字段:
- 错误码:
- 是否需要幂等:
- 是否涉及数据库:
- 是否涉及外部服务:
请输出:
1. 正常场景
2. 参数缺失场景
3. 参数非法场景
4. 权限 / 鉴权场景
5. 资源不存在场景
6. 业务状态异常场景
7. 重复提交场景
8. 并发场景
9. 外部服务失败场景
10. 返回结构一致性场景
每条用例包含:
- 用例名称
- 输入条件
- 预期状态码
- 预期 code
- 是否 P0
- 是否必须自动化
这个模板可以放进项目文档:
docs/ai-test-template.md
团队成员以后写接口时,先补测试矩阵,再补测试代码。
这样 AI 不是随便生成几条测试,而是按团队规范输出。

#九、哪些测试适合 AI 写,哪些不适合完全交给 AI?
适合 AI 辅助生成的测试:
| 测试类型 | 适合程度 | 原因 |
|---|---|---|
| 参数校验测试 | 高 | 规则明确,容易枚举 |
| 错误码测试 | 高 | 可以根据错误码表生成 |
| 返回结构测试 | 高 | 格式固定,适合自动化 |
| 基础接口测试 | 高 | Supertest 这类模式稳定 |
| 边界值测试 | 中高 | AI 可以提醒,但人要确认业务边界 |
| 权限测试 | 中 | AI 可列场景,规则必须人工确认 |
| 并发测试 | 中低 | 需要真实环境和压测工具 |
| 支付 / 订单核心链路 | 中低 | 可以辅助,但不能只靠 AI |
| 数据一致性测试 | 中低 | 需要结合数据库事务和真实业务 |
尤其涉及支付、权限、库存、数据删除、风控、隐私数据的接口,AI 生成的测试只能作为起点。
最终还是要开发者根据业务确认。
#十、gpt 的位置:工具只是入口,测试流程才是关键
如果你长期使用 ChatGPT Plus、Claude Pro、Grok、Gemini 这类 AI 工具,也可以顺手了解 gpt328它是第三方 AI 会员充值平台,可作为 AI 工具订阅充值入口之一,但不是 OpenAI、Anthropic、Google、xAI、Cursor、Kiro 官方网站或授权合作方。
使用前建议看清楚套餐说明、账号要求、到账说明和售后规则。
不过对开发者来说,真正决定效率的不是“AI 能不能帮你写测试”,而是你有没有把接口设计表、测试矩阵、测试代码、人工 Review 这几步串起来。
工具只是入口,测试流程才是质量保障。
#十一、结论
AI 生成接口测试很快,但不能只测成功场景。
一个可靠的接口测试,至少应该覆盖:
正常流程;
参数缺失;
参数非法;
边界值;
鉴权失败;
资源不存在;
业务异常;
重复提交;
返回结构一致性;
错误码一致性。
让 AI 写测试时,不要一上来就说“帮我写 Jest 测试”。
更稳的顺序是:
先给接口设计表;
再让 AI 生成测试矩阵;
再确认 P0 场景;
再生成测试代码;
再运行测试;
最后人工 Review。
接口能跑,不代表接口可靠。
测试绿了,也不代表测试完整。
真正有价值的 AI 测试,不是多生成几行 test(),而是帮你把那些容易漏掉的异常、边界和联调风险提前暴露出来。
更多推荐
所有评论(0)