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 9c5a7630a4
commit 355d19418b
10 changed files with 1263 additions and 182 deletions
+207 -32
View File
@@ -90,24 +90,25 @@ func (a *Api) File(field, name string, content []byte) *ApiCase {
return c
}
// Get 直接发送 GET 请求(无数据场景)
func (a *Api) Get(desc string, status int, msg ...string) *ApiResponse {
return a.newCase().Get(desc, status, msg...)
// Get 直接发送 GET 请求
// expect 可选参数: string 为期望 msgMap 为期望 result 结构(按样本值类型校验)
func (a *Api) Get(desc string, status int, expect ...interface{}) *ApiResponse {
return a.newCase().Get(desc, status, expect...)
}
// Post 直接发送 POST 请求(无数据场景)
func (a *Api) Post(desc string, status int, msg ...string) *ApiResponse {
return a.newCase().Post(desc, status, msg...)
// Post 直接发送 POST 请求
func (a *Api) Post(desc string, status int, expect ...interface{}) *ApiResponse {
return a.newCase().Post(desc, status, expect...)
}
// Put 直接发送 PUT 请求(无数据场景)
func (a *Api) Put(desc string, status int, msg ...string) *ApiResponse {
return a.newCase().Put(desc, status, msg...)
// Put 直接发送 PUT 请求
func (a *Api) Put(desc string, status int, expect ...interface{}) *ApiResponse {
return a.newCase().Put(desc, status, expect...)
}
// Delete 直接发送 DELETE 请求(无数据场景)
func (a *Api) Delete(desc string, status int, msg ...string) *ApiResponse {
return a.newCase().Delete(desc, status, msg...)
// Delete 直接发送 DELETE 请求
func (a *Api) Delete(desc string, status int, expect ...interface{}) *ApiResponse {
return a.newCase().Delete(desc, status, expect...)
}
// Note 为下一条用例设置备注,返回可继续链式的 ApiCase
@@ -142,6 +143,7 @@ type ApiCase struct {
fileName string
fileContent []byte
note string
verifyFn func(a *Api) error
}
// JSON 叠加 JSON body
@@ -194,27 +196,53 @@ func (c *ApiCase) Note(note string) *ApiCase {
return c
}
// Verify 设置请求后的自定义校验函数(如查库验证数据状态)
// 函数返回 nil 表示校验通过,返回 error 则用例失败并记录错误原因
func (c *ApiCase) Verify(fn func(a *Api) error) *ApiCase {
c.verifyFn = fn
return c
}
// Get 终端操作:GET 请求 + 断言
func (c *ApiCase) Get(desc string, status int, msg ...string) *ApiResponse {
return c.execute("GET", desc, status, msg...)
func (c *ApiCase) Get(desc string, status int, expect ...interface{}) *ApiResponse {
return c.execute("GET", desc, status, expect...)
}
// Post 终端操作:POST 请求 + 断言
func (c *ApiCase) Post(desc string, status int, msg ...string) *ApiResponse {
return c.execute("POST", desc, status, msg...)
func (c *ApiCase) Post(desc string, status int, expect ...interface{}) *ApiResponse {
return c.execute("POST", desc, status, expect...)
}
// Put 终端操作:PUT 请求 + 断言
func (c *ApiCase) Put(desc string, status int, msg ...string) *ApiResponse {
return c.execute("PUT", desc, status, msg...)
func (c *ApiCase) Put(desc string, status int, expect ...interface{}) *ApiResponse {
return c.execute("PUT", desc, status, expect...)
}
// Delete 终端操作:DELETE 请求 + 断言
func (c *ApiCase) Delete(desc string, status int, msg ...string) *ApiResponse {
return c.execute("DELETE", desc, status, msg...)
func (c *ApiCase) Delete(desc string, status int, expect ...interface{}) *ApiResponse {
return c.execute("DELETE", desc, status, expect...)
}
func (c *ApiCase) execute(method, desc string, expectStatus int, expectMsg ...string) *ApiResponse {
func (c *ApiCase) execute(method, desc string, expectStatus int, expectArgs ...interface{}) *ApiResponse {
// status!=0 时: 第一个 string → msg 校验,其余 → result 结构校验
// status==0 时: 所有参数 → result 类型/结构校验(因为成功响应没有 msg)
var expectMsg string
var expectResult interface{}
var hasExpectResult bool
for _, arg := range expectArgs {
if s, ok := arg.(string); ok && expectStatus != 0 && expectMsg == "" {
expectMsg = s
continue
}
if !hasExpectResult {
expectResult = arg
hasExpectResult = true
}
}
if expectStatus != 0 && expectMsg == "" {
expectMsg = desc
}
t := c.api.t
var resp *ApiResponse
@@ -229,7 +257,11 @@ func (c *ApiCase) execute(method, desc string, expectStatus int, expectMsg ...st
body := Map{}
if len(w.Body.Bytes()) > 0 {
_ = json.Unmarshal(w.Body.Bytes(), &body)
if obj, err := JsonToObj(w.Body.String()); err == nil {
if m, ok := obj.(map[string]interface{}); ok {
body = Map(m)
}
}
}
resp = &ApiResponse{
@@ -246,22 +278,38 @@ func (c *ApiCase) execute(method, desc string, expectStatus int, expectMsg ...st
passed = false
}
if passed && len(expectMsg) > 0 && expectMsg[0] != "" {
if passed && expectMsg != "" {
actualMsg := resp.GetMsg()
if actualMsg != expectMsg[0] {
t.Errorf("消息不匹配: 期望 %q, 实际 %q\n响应: %s", expectMsg[0], actualMsg, w.Body.String())
if actualMsg != expectMsg {
t.Errorf("消息不匹配: 期望 %q, 实际 %q\n响应: %s", expectMsg, actualMsg, w.Body.String())
passed = false
}
}
if passed && hasExpectResult {
if !validateShape(body["result"], expectResult, "result", t) {
passed = false
}
}
var verifyError string
if passed && c.verifyFn != nil {
if err := c.verifyFn(c.api); err != nil {
t.Errorf("Verify 校验失败: %s", err.Error())
passed = false
verifyError = err.Error()
}
}
record := TestRecord{
Path: c.api.path,
Desc: desc,
CaseName: desc,
Passed: passed,
Duration: duration,
Method: method,
Note: c.note,
Path: c.api.path,
Desc: desc,
CaseName: desc,
Passed: passed,
Duration: duration,
Method: method,
Note: c.note,
VerifyError: verifyError,
}
if c.query != nil {
record.Query = c.query
@@ -279,6 +327,21 @@ func (c *ApiCase) execute(method, desc string, expectStatus int, expectMsg ...st
if len(body) > 0 {
record.ResponseBody = body
}
expectResp := Map{"status": expectStatus}
if expectStatus != 0 {
if expectMsg != "" {
errorInfo := Map{"msg": expectMsg}
errorType := c.api.app.Config.GetMap("error").GetString(ObjToStr(expectStatus))
if errorType != "" {
errorInfo["type"] = errorType
}
expectResp["result"] = errorInfo
expectResp["error"] = errorInfo
}
} else if hasExpectResult {
expectResp["result"] = expectResult
}
record.ExpectResult = expectResp
c.api.app.collector.Add(record)
})
@@ -390,3 +453,115 @@ func (r *ApiResponse) Fail(msg string) {
fmt.Printf("自定义断言失败: %s\n", msg)
}
}
// ExpectResult 后置校验 result 结构,expected 作为类型样本(只比类型,不比值)
// expected 可以是 Map(校验字段)、Slice(校验数组)、或 string/int64/float64 等原始类型
//
// res.ExpectResult(Map{"id": int64(1), "name": "样本"})
// res.ExpectResult("操作成功") // 校验 result 是 string 类型
// res.ExpectResult(Slice{Map{"id": int64(1)}}) // 校验 result 是数组
func (r *ApiResponse) ExpectResult(expected interface{}) *ApiResponse {
if !validateShape(r.Body["result"], expected, "result", r.t) && r.t == nil {
fmt.Printf("ExpectResult 校验失败\n")
}
return r
}
// === 结构校验 ===
// validateShape 递归校验 actual 是否符合 expected 的结构(字段名+类型大类)
func validateShape(actual, expected interface{}, path string, t *testing.T) bool {
if expected == nil {
return true
}
if actual == nil {
if t != nil {
t.Errorf("字段 %s 缺失", path)
}
return false
}
switch exp := expected.(type) {
case Map:
return validateMapShape(actual, exp, path, t)
case map[string]interface{}:
return validateMapShape(actual, Map(exp), path, t)
case Slice:
return validateSliceShape(actual, exp, path, t)
case []interface{}:
return validateSliceShape(actual, Slice(exp), path, t)
default:
if !sameTypeCategory(actual, expected) {
if t != nil {
t.Errorf("字段 %s 类型不匹配: 期望 %s, 实际 %s(%v)",
path, typeCategory(expected), typeCategory(actual), actual)
}
return false
}
return true
}
}
func validateMapShape(actual interface{}, exp Map, path string, t *testing.T) bool {
actualMap := ObjToMap(actual)
if actualMap == nil {
if t != nil {
t.Errorf("字段 %s 类型不匹配: 期望 map, 实际 %s", path, typeCategory(actual))
}
return false
}
ok := true
for field, expectedVal := range exp {
fieldPath := path + "." + field
actualVal, exists := actualMap[field]
if !exists {
if t != nil {
t.Errorf("字段 %s 缺失", fieldPath)
}
ok = false
continue
}
if !validateShape(actualVal, expectedVal, fieldPath, t) {
ok = false
}
}
return ok
}
func validateSliceShape(actual interface{}, exp Slice, path string, t *testing.T) bool {
actualSlice := ObjToSlice(actual)
if actualSlice == nil {
if t != nil {
t.Errorf("字段 %s 类型不匹配: 期望 slice, 实际 %s", path, typeCategory(actual))
}
return false
}
if len(exp) > 0 && len(actualSlice) > 0 {
return validateShape(actualSlice[0], exp[0], path+"[0]", t)
}
return true
}
func sameTypeCategory(a, b interface{}) bool {
return typeCategory(a) == typeCategory(b)
}
func typeCategory(v interface{}) string {
switch v.(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64:
return "number"
case float32, float64:
return "float"
case string:
return "string"
case bool:
return "bool"
case Map, map[string]interface{}:
return "map"
case Slice, []interface{}:
return "slice"
default:
return fmt.Sprintf("%T", v)
}
}