很多后端接口上线前,测试看起来是有的。

比如创建订单接口,有一条测试:

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=1201,返回 orderIdP0
鉴权失败未登录创建订单缺少用户身份401,code=UNAUTHORIZEDP0
参数缺失缺少 productIdbody 无 productId400,code=VALIDATION_ERRORP0
参数缺失缺少 quantitybody 无 quantity400,code=VALIDATION_ERRORP0
参数边界quantity=0数量为 0400,code=VALIDATION_ERRORP0
参数边界quantity=-1数量为负数400,code=VALIDATION_ERRORP0
资源不存在商品不存在productId 不存在404,code=PRODUCT_NOT_FOUNDP0
业务异常库存不足quantity 大于库存409,code=ORDER_STOCK_NOT_ENOUGHP0
幂等处理重复提交相同 idempotency-key 提交两次第二次 409P0
返回结构所有失败响应检查 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(),而是帮你把那些容易漏掉的异常、边界和联调风险提前暴露出来。

更多推荐