feat(api): 增强 API 测试框架的断言功能
- 修改 ApiCase 和 ApiResponse 结构,新增 Verify 方法以支持自定义校验函数 - 更新 Get、Post、Put、Delete 方法,允许使用 expect 参数进行更灵活的响应断言 - 添加 ExpectResult 方法,支持对响应结果的结构和类型进行后置校验 - 更新文档,详细说明新功能的使用方法和示例,提升测试框架的可用性 - 增强 Swagger 生成逻辑,支持输出新的 expect 和 verifyError 字段
This commit is contained in:
+104
-2
@@ -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)})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user