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