从需求与接口文档生成测试用例
测试用例最贵的部分是「想全」。需求条目、接口文档(OpenAPI/protobuf)、权限矩阵、历史缺陷单都是模型擅长处理的文本;让 AI 把它们摊成用例清单,人负责删掉幻觉、补上判定依据。
1. 输入准备:把文档裁成可判定的片段
不要整篇丢进去。按接口切开,一次一份,并明确字段的合法取值范围。
# openapi/order.yaml 片段
paths:
/api/orders:
post:
parameters: [{ in: header, name: Idempotency-Key, required: false }]
requestBody:
required: true
schema: { required: [sku, qty], sku: { minLength: 1, maxLength: 32 }, qty: { minimum: 1, maximum: 999 } }
responses: { "201": 创建成功, "400": 参数非法, "409": 库存不足 }
角色:测试工程师。
输入:上面的接口片段 + 业务规则(同一用户对同一 sku 每日限购 10 件;带相同
Idempotency-Key 的重复请求必须返回首次结果,且不重复扣库存)。
任务:生成用例清单,用 Markdown 表格输出。
要求:
1. 分类固定为:正常、边界、异常、权限、幂等;
2. 每行含:用例 ID、分类、前置、输入、期望结果、判定依据、优先级、是否可自动化;
3. 期望结果必须可判定(状态码 + 响应体关键字段),不许写「正常返回」;
4. 只使用文档中出现的字段与状态码;没覆盖的判断点单列「待确认」;
5. 不要输出测试代码。
第 4 条最关键:模型很愿意「帮你想一个合理的字段」,比如给订单补个 coupon_code、给错误编一个 422。
2. 用例表长什么样
| ID | 分类 | 前置 | 输入 | 期望结果 | 判定依据 | 优先级 |
|---|---|---|---|---|---|---|
| ORD-01 | 正常 | 库存 100 | sku=A1, qty=1 | 201,id 非空,库存 99 | OpenAPI 201 | P0 |
| ORD-02 | 边界 | 库存 100 | qty=1(最小值) | 201 | minimum: 1 | P0 |
| ORD-03 | 边界 | 库存 100 | qty=999(最大值) | 201 | maximum: 999 | P1 |
| ORD-04 | 边界 | 库存 100 | qty=1000 | 400,error=PARAM_INVALID | maximum: 999 | P0 |
| ORD-05 | 边界 | 今日已下 9 件 | qty=1 | 201(恰好到限购) | 限购 10 件 | P0 |
| ORD-06 | 边界 | 今日已下 10 件 | qty=1 | 400,error=LIMIT_EXCEEDED | 限购 10 件 | P0 |
| ORD-07 | 异常 | 库存 0 | qty=1 | 409,error=OUT_OF_STOCK | OpenAPI 409 | P0 |
| ORD-08 | 异常 | — | 缺 sku 字段 | 400,不是 500 | required: [sku, qty] | P0 |
| ORD-09 | 异常 | — | sku 长度 33 | 400 | maxLength: 32 | P1 |
| ORD-10 | 权限 | 未带令牌 | 合法 body | 401 | 鉴权中间件 | P0 |
| ORD-11 | 权限 | 令牌属于他人 | 查询他人订单 | 404,不泄露存在性 | 越权防护 | P0 |
| ORD-12 | 幂等 | 已有 Key=K1 的成功请求 | 同 Key 重放 | 返回首次结果,库存只扣一次 | 幂等规则 | P0 |
| ORD-13 | 幂等 | 并发发起 2 个同 Key 请求 | 同时提交 | 仅 1 个 201,另 1 个返回同一结果 | 幂等 + 并发 | P0 |
「判定依据」是人工必填项:它逼你把「为什么是这个期望值」落到文档的某一行,而不是模型的印象。
3. 用例转测试骨架
清单确认后再转代码,一次一个分类,避免生成一大坨跑不起来的东西。
角色:Go 测试工程师。
输入:表格里的 ORD-01 ~ ORD-09。
要求:
1. 用 httptest + 表驱动,name 直接用用例 ID 与描述;
2. 只生成骨架:请求构造、状态码断言、响应体关键字段断言;
3. 库存与限购计数通过替身的初始状态控制;
4. 表格里没有的用例不要自行添加。
func TestCreateOrder(t *testing.T) {
cases := []struct {
id, desc string
stock, todayQty int
body string
wantCode int
wantErr string
}{
{"ORD-01", "正常创建", 100, 0, `{"sku":"A1","qty":1}`, 201, ""},
{"ORD-04", "超最大数量", 100, 0, `{"sku":"A1","qty":1000}`, 400, "PARAM_INVALID"},
{"ORD-06", "超出限购", 100, 10, `{"sku":"A1","qty":1}`, 400, "LIMIT_EXCEEDED"},
{"ORD-07", "库存不足", 0, 0, `{"sku":"A1","qty":1}`, 409, "OUT_OF_STOCK"},
{"ORD-08", "缺少 sku", 100, 0, `{"qty":1}`, 400, "PARAM_INVALID"},
}
for _, tc := range cases {
t.Run(tc.id+" "+tc.desc, func(t *testing.T) {
r := newTestRouter(tc.stock, tc.todayQty)
req := httptest.NewRequest(http.MethodPost, "/api/orders", strings.NewReader(tc.body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+testToken)
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != tc.wantCode {
t.Fatalf("code = %d, want %d, body = %s", rec.Code, tc.wantCode, rec.Body.String())
}
var got struct{ Error string `json:"error"` }
_ = json.Unmarshal(rec.Body.Bytes(), &got)
if tc.wantErr != "" && got.Error != tc.wantErr {
t.Errorf("error = %q, want %q", got.Error, tc.wantErr)
}
})
}
}
4. AI 擅长与人工必补
| 维度 | AI 表现 | 人工必补 |
|---|---|---|
| 正常路径 | 全 | 是否有多个业务分支(新客/老客) |
| 边界值 | 强(min/max 直接读文档) | 业务侧隐式边界(限购、额度、时段) |
| 异常路径 | 中(易编造状态码) | 错误码契约、是否泄露内部信息 |
| 权限 | 弱(文档通常没写) | 越权、角色矩阵、数据归属 |
| 幂等与并发 | 弱 | 幂等键、重复请求、并发同键 |
| 数据一致性 | 弱 | 库存与账户扣减只发生一次的验证 |
5. 常见坑
- 幻觉字段与状态码:模型补出文档里没有的
coupon_code、422。验收时把生成的字段名逐个回查 OpenAPI,不存在的删掉。 - 不可判定的期望:出现「正常返回」「应当合理」这类描述,用例等于没写;判定依据必须能指向文档行或业务规则。
- 把实现当需求:直接贴代码让 AI 生成用例,得到的是「实现现在怎么做」而不是「需求要求怎么做」,缺陷会被继承。
- 漏掉权限与幂等:AI 默认只从功能路径出发,这两类必须显式写进要求的分类里,否则基本不会出现。
- 一次要太多:一次喂十个接口,输出表格字段错位、ID 重复。按接口切开,逐个生成再合并。
- 用例没有稳定 ID:没有 ID 就无法做「需求变更 → 用例增删」的追踪,也无法在缺陷单里引用;ID 一经分配不要复用。
小结:把 AI 当「用例清单的扩写器」——输入是切好的接口文档与业务规则,输出是分类齐全的表格,其中判定依据一列必须由人回查文档确认;幂等、权限与并发这三类要显式点名要求,清单定稿后再批量转测试骨架,并保留稳定的用例 ID 用于追踪。