feat(api): 增强 API 测试框架的断言功能

- 修改 ApiCase 和 ApiResponse 结构,新增 Verify 方法以支持自定义校验函数
- 更新 Get、Post、Put、Delete 方法,允许使用 expect 参数进行更灵活的响应断言
- 添加 ExpectResult 方法,支持对响应结果的结构和类型进行后置校验
- 更新文档,详细说明新功能的使用方法和示例,提升测试框架的可用性
- 增强 Swagger 生成逻辑,支持输出新的 expect 和 verifyError 字段
This commit is contained in:
2026-03-21 13:38:22 +08:00
parent 813e26755f
commit 70012032f0
10 changed files with 1263 additions and 182 deletions
+104 -2
View File
@@ -1,4 +1,4 @@
# HoTime API 测试框架使用说明
# HoTime API 测试框架使用说明
不启动 HTTP 服务、自动事务回滚、链式 API、覆盖率报告、API 调试控制台生成。
@@ -229,6 +229,7 @@ func (app *TestApp) DB() *HoTimeDB
| `c.File(field, name, content)` | 设置上传文件 | `*ApiCase` |
| `c.Note(note)` | 为本条用例添加可选备注,备注显示在调试控制台用例详情中 | `*ApiCase` |
| `c.WithSession(s)` | 覆盖本次请求的 session | `*ApiCase` |
| `c.Verify(fn)` | 设置请求后的自定义校验函数(详见 [Verify 数据库校验](#verify-数据库校验) | `*ApiCase` |
| `c.Get(desc, status, msg...)` | 终端:发送 GET 请求并断言 | `*ApiResponse` |
| `c.Post(desc, status, msg...)` | 终端:发送 POST 请求并断言 | `*ApiResponse` |
| `c.Put(desc, status, msg...)` | 终端:发送 PUT 请求并断言 | `*ApiResponse` |
@@ -240,7 +241,48 @@ func (app *TestApp) DB() *HoTimeDB
|------|------|------|
| `desc` | string | 用例描述,作为 `t.Run` 的子测试名称 |
| `status` | int | 期望的 JSON 响应体中 `status` 字段值(0=成功) |
| `msg` | ...string | 可选,期望的 `msg` 字段值(传空则不校验 |
| `expect` | ...interface{} | 可选,行为取决于 status(见下方说明 |
**expect 参数解析规则:**
- `status == 0`(成功):
- **不传 expect** → 仅校验 status=0,不校验 result 内容(如 `a.Post("描述", 0)`
- 传任意类型 → result 类型/结构校验(因为成功响应没有 msg)
- `status != 0`(错误):
-`string` → 用该字符串校验错误消息
- **不传 expect** → 自动用 `desc`(第一参数)作为期望的错误消息(简写模式)
- 传非 string 类型 → 按 status=0 的规则做 result 结构校验(罕见但支持)
**简写示例**(错误用例 desc 即 msg):
```go
// 完整写法
a.Post("密码为空", 4, "用户名或密码不能为空") // desc="密码为空", 校验 msg="用户名或密码不能为空"
// 简写:desc 与错误消息相同时可省略第三参数
a.Post("用户名或密码不能为空", 4) // desc 直接作为期望 msg
a.Get("请选择店铺", 3) // 等价于 a.Get("请选择店铺", 3, "请选择店铺")
```
**result 类型/结构校验:**
expect 参数作为**类型样本**,只比较类型大类,不比较具体值。result 不一定是 Map,可以是任意类型:
| 样本值类型 | 匹配的实际类型 | 说明 |
|-----------|--------------|------|
| `int64` / `int` 等整数 | 整数类型 | number(整数) |
| `float64` / `float32` | 浮点类型 | float(小数) |
| `string` | `string` | 字符串 |
| `bool` | `bool` | 布尔值 |
| `Map{...}` | `Map` / `map[string]interface{}` | 对象,递归校验子字段 |
| `Slice{...}` | `Slice` / `[]interface{}` | 数组,非空时校验首元素结构 |
校验规则:
- result 是原始类型(string、int、float 等)→ 直接比较类型大类
- result 是 Map → **多了**字段不管,**少了**预设字段报错「字段缺失」
- 字段存在但**类型不一致** → 报错「类型不匹配」
- Map 类型的值 → 递归验证子字段
- Slice 类型的值且样本非空 → 取实际首元素与样本首元素递归验证
### ApiResponse — 响应对象
@@ -255,6 +297,10 @@ resp.GetMsg() // string:业务 msg
resp.GetResult() // interface{}result 字段
resp.GetBody() // Map:完整响应体
// 后置结构校验(支持 Map、Slice、string、int64 等任意类型样本)
resp.ExpectResult(Map{"id": int64(1), "token": "sample"})
resp.ExpectResult("操作成功") // 校验 result 是 string 类型
// 自定义断言
token := resp.GetBody().GetMap("result").GetString("token")
if token == "" {
@@ -314,6 +360,62 @@ resp := a.JSON(Map{"name": "138", "password": "123"}).Post("正常登录", 0)
if resp.GetBody().GetMap("result").GetString("token") == "" {
resp.Fail("响应中缺少 token 字段")
}
// ========== Result 类型/结构校验 ==========
// result 是原始类型:校验 result 是 string
a.Post("操作成功", 0, "操作成功")
// result 是整数
a.Get("获取数量", 0, int64(1))
// result 是小数
a.Get("获取金额", 0, float64(1.5))
// result 是 Map:校验字段名 + 类型
a.Query(Map{"shop_id": "1", "id": "1"}).Get("正常获取订单", 0, Map{
"id": int64(1), "sn": "sample", "customer_id": int64(1),
})
// 嵌套对象:递归校验子字段
a.Get("获取详情", 0, Map{
"id": int64(1),
"customer": Map{"name": "张三", "id": int64(1)},
})
// result 直接是数组:校验 result 是 Slice 且首元素结构匹配
a.Get("获取标签", 0, Slice{Map{"id": int64(1), "name": "test"}})
// Map 中包含数组字段
a.JSON(Map{"page": 1, "pageSize": 10}).Post("获取列表", 0, Map{
"total": int64(10),
"data": Slice{Map{"id": int64(1), "name": "test"}},
})
// 后置校验
resp = a.Get("获取配置", 0)
resp.ExpectResult(Map{"version": "1.0", "features": Slice{}})
// ========== Verify 数据库校验 ==========
// 请求后自动查库验证,返回 nil 表示通过,返回 error 则用例失败
a.Form(Map{"name": "test"}).
Verify(func(a *Api) error {
row := a.DB().Get("order", "*", Map{"name": "test"})
if row == nil {
return fmt.Errorf("未找到 name=test 的记录")
}
if row.GetCeilInt64("state") != 0 {
return fmt.Errorf("state 期望 0, 实际 %d", row.GetCeilInt64("state"))
}
// 校验关联表数量
count := a.DB().Count("order_goods", Map{"order_id": row.GetCeilInt64("id")})
if count < 1 {
return fmt.Errorf("order_goods 期望至少 1 条, 实际 %d 条", count)
}
return nil
}).
Post("创建并验证DB", 0, Map{"id": int64(1)})
```
---