From 355d19418b368f1adaf2a905e56fdb42fe8c8d2f Mon Sep 17 00:00:00 2001 From: hoteas <925970985@qq.com> Date: Sat, 21 Mar 2026 13:38:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E5=A2=9E=E5=BC=BA=20API=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=A1=86=E6=9E=B6=E7=9A=84=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 ApiCase 和 ApiResponse 结构,新增 Verify 方法以支持自定义校验函数 - 更新 Get、Post、Put、Delete 方法,允许使用 expect 参数进行更灵活的响应断言 - 添加 ExpectResult 方法,支持对响应结果的结构和类型进行后置校验 - 更新文档,详细说明新功能的使用方法和示例,提升测试框架的可用性 - 增强 Swagger 生成逻辑,支持输出新的 expect 和 verifyError 字段 --- docs/Testing_API测试框架.md | 106 +++- example/app/app_test.go | 9 +- example/app/expect_demo_test.go | 81 +++ example/app/init.go | 9 +- example/app/test.go | 61 ++ example/tpt/swagger/app/api-spec.json | 828 ++++++++++++++++++++++---- example/tpt/swagger/app/index.html | 40 +- testing_api.go | 239 +++++++- testing_helper.go | 2 + testing_swagger.go | 70 ++- 10 files changed, 1263 insertions(+), 182 deletions(-) create mode 100644 example/app/expect_demo_test.go diff --git a/docs/Testing_API测试框架.md b/docs/Testing_API测试框架.md index 1ef7a37..677e1aa 100644 --- a/docs/Testing_API测试框架.md +++ b/docs/Testing_API测试框架.md @@ -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)}) ``` --- diff --git a/example/app/app_test.go b/example/app/app_test.go index 8206ad3..ff452cd 100644 --- a/example/app/app_test.go +++ b/example/app/app_test.go @@ -8,10 +8,11 @@ import ( ) var ProjectTest = ProjTest{ - "test": TestTest, - "mysql": MysqlTest, - "dmdb": DmdbTest, - "cache": CacheTest, + "test": TestTest, + "mysql": MysqlTest, + "dmdb": DmdbTest, + "cache": CacheTest, + "expect_demo": ExpectDemoTest, } var testApp *TestApp diff --git a/example/app/expect_demo_test.go b/example/app/expect_demo_test.go new file mode 100644 index 0000000..ce44fc5 --- /dev/null +++ b/example/app/expect_demo_test.go @@ -0,0 +1,81 @@ +package app + +import ( + "fmt" + + . "code.hoteas.com/golang/hotime" + . "code.hoteas.com/golang/hotime/common" +) + +var ExpectDemoTest = CtrTest{ + "string_result": {Desc: "返回字符串", Func: func(a *Api) { + a.Get("result是字符串", 0, "操作成功") + }}, + "number_result": {Desc: "返回整数", Func: func(a *Api) { + a.Get("result是整数", 0, int64(1)) + }}, + "float_result": {Desc: "返回小数", Func: func(a *Api) { + a.Get("result是小数", 0, float64(1.0)) + }}, + "bool_result": {Desc: "返回布尔", Func: func(a *Api) { + a.Get("result是布尔", 0, true) + }}, + "map_result": {Desc: "返回对象", Func: func(a *Api) { + a.Get("result是Map-校验字段名和类型", 0, Map{ + "id": int64(1), "name": "sample", "price": float64(1.0), "in_stock": true, + }) + }}, + "nested_result": {Desc: "返回嵌套对象", Func: func(a *Api) { + a.Get("深层嵌套Map+Slice+Map", 0, Map{ + "id": int64(1), "title": "sample", + "customer": Map{"id": int64(1), "name": "sample", "phone": "sample"}, + "items": Slice{Map{ + "id": int64(1), "goods_name": "sample", "quantity": int64(1), "price": float64(1.0), + }}, + "extra": Map{ + "delivery": Map{"address": "sample", "fee": int64(1)}, + }, + }) + }}, + "slice_result": {Desc: "返回数组", Func: func(a *Api) { + a.Get("result直接是Slice", 0, Slice{Map{"id": int64(1), "name": "sample"}}) + }}, + "error_demo": {Desc: "错误+成功混合", Func: func(a *Api) { + a.Form(Map{"name": ""}).Post("名称为空-错误断言", 3, "名称不能为空") + a.Form(Map{"name": "测试"}).Post("名称正确-结构校验", 0, Map{ + "id": int64(1), "name": "sample", "created": true, + }) + }}, + "no_expect": {Desc: "不传预期(仅校验status)", Func: func(a *Api) { + a.Get("只校验status=0", 0) + }}, + "verify_demo": {Desc: "Verify数据库校验", Func: func(a *Api) { + a.Form(Map{"name": ""}).Post("名称为空", 3, "名称不能为空") + + a.Form(Map{"name": "verify_test"}). + Verify(func(a *Api) error { + row := a.DB().Get("test_batch", "*", Map{"AND": Map{"name": "verify_test", "title": "verify_demo"}}) + if row == nil { + return fmt.Errorf("test_batch 表未找到 name=verify_test 的记录") + } + if row.GetCeilInt64("state") != 0 { + return fmt.Errorf("test_batch.state 期望 0, 实际 %d", row.GetCeilInt64("state")) + } + return nil + }). + Post("写入并校验DB-正确", 0, Map{"id": int64(1), "name": "sample"}) + + a.Form(Map{"name": "verify_fail"}). + Verify(func(a *Api) error { + row := a.DB().Get("test_batch", "*", Map{"AND": Map{"name": "verify_fail", "title": "verify_demo"}}) + if row == nil { + return fmt.Errorf("test_batch 表未找到 name=verify_fail 的记录") + } + if row.GetCeilInt64("state") != 99 { + return fmt.Errorf("test_batch.state 期望 99, 实际 %d(接口写入的是0,这里故意校验99来演示失败效果)", row.GetCeilInt64("state")) + } + return nil + }). + Post("写入并校验DB-故意失败", 0, Map{"id": int64(1), "name": "sample"}) + }}, +} diff --git a/example/app/init.go b/example/app/init.go index c8076b9..ff96151 100644 --- a/example/app/init.go +++ b/example/app/init.go @@ -5,8 +5,9 @@ import ( ) var Project = Proj{ - "test": TestCtr, - "mysql": MysqlCtr, - "dmdb": DmdbCtr, - "cache": CacheCtr, + "test": TestCtr, + "mysql": MysqlCtr, + "dmdb": DmdbCtr, + "cache": CacheCtr, + "expect_demo": ExpectDemoCtr, } diff --git a/example/app/test.go b/example/app/test.go index 996b9d6..54a422f 100644 --- a/example/app/test.go +++ b/example/app/test.go @@ -43,6 +43,67 @@ func nowFunc(that *Context) string { return "NOW()" } +// ExpectDemoCtr 用于展示 result 结构校验的各种场景 +var ExpectDemoCtr = Ctr{ + "string_result": func(that *Context) { + that.Display(0, "操作成功") + }, + "number_result": func(that *Context) { + that.Display(0, 42) + }, + "float_result": func(that *Context) { + that.Display(0, 3.14) + }, + "bool_result": func(that *Context) { + that.Display(0, true) + }, + "map_result": func(that *Context) { + that.Display(0, Map{ + "id": 1, "name": "示例商品", "price": 19.9, "in_stock": true, + }) + }, + "nested_result": func(that *Context) { + that.Display(0, Map{ + "id": 100, "title": "测试订单", + "customer": Map{"id": 1, "name": "张三", "phone": "13800138000"}, + "items": Slice{ + Map{"id": 1, "goods_name": "苹果", "quantity": 5, "price": 3.5}, + Map{"id": 2, "goods_name": "香蕉", "quantity": 10, "price": 2.0}, + }, + "extra": Map{ + "delivery": Map{"address": "XX路1号", "fee": 5}, + }, + }) + }, + "slice_result": func(that *Context) { + that.Display(0, Slice{ + Map{"id": 1, "name": "标签A"}, + Map{"id": 2, "name": "标签B"}, + }) + }, + "error_demo": func(that *Context) { + name := that.Req.FormValue("name") + if name == "" { + that.Display(3, "名称不能为空") + return + } + that.Display(0, Map{"id": 1, "name": name, "created": true}) + }, + "no_expect": func(that *Context) { + that.Display(0, Map{"id": 1, "data": "无预期校验"}) + }, + "verify_demo": func(that *Context) { + name := that.Req.FormValue("name") + if name == "" { + that.Display(3, "名称不能为空") + return + } + initTestTables(that) + id := that.Db.Insert("test_batch", Map{"name": name, "title": "verify_demo", "state": 0}) + that.Display(0, Map{"id": id, "name": name}) + }, +} + // TestCtr 通用 ORM 测试控制器(数据库无关) var TestCtr = Ctr{ "test": func(that *Context) { diff --git a/example/tpt/swagger/app/api-spec.json b/example/tpt/swagger/app/api-spec.json index e606728..0c7cb30 100644 --- a/example/tpt/swagger/app/api-spec.json +++ b/example/tpt/swagger/app/api-spec.json @@ -97,6 +97,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -119,6 +122,9 @@ "message": "批量缓存测试完成,请查看控制台输出和日志文件" }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -165,7 +171,7 @@ "key_in_new_table": false, "name": "4. 测试兼容模式老表回退读取", "result": false, - "test_key": "test_compat_fallback_1773975517169601300" + "test_key": "test_compat_fallback_1774071258597810000" }, { "name": "5. 测试兼容模式写新删老", @@ -183,9 +189,12 @@ "result": false } ], - "timestamp": "2026-03-20 10:58:37" + "timestamp": "2026-03-21 13:34:18" }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -266,9 +275,9 @@ ], "sample_data": [ { - "create_time": "2026-03-20T10:28:27.135243+08:00", + "create_time": "2026-03-20 10:28:27", "id": 1, - "modify_time": "2026-03-20T10:28:27.135244+08:00", + "modify_time": "2026-03-20 10:28:27", "name": "超级管理员", "password": "admin123", "phone": "13800000001", @@ -277,9 +286,9 @@ "title": "系统管理员" }, { - "create_time": "2026-03-20T10:28:27.172725+08:00", + "create_time": "2026-03-20 10:28:27", "id": 2, - "modify_time": "2026-03-20T10:28:27.172727+08:00", + "modify_time": "2026-03-20 10:28:27", "name": "编辑员", "password": "editor123", "phone": "13800000002", @@ -288,9 +297,9 @@ "title": "内容编辑" }, { - "create_time": "2026-03-20T10:28:27.208006+08:00", + "create_time": "2026-03-20 10:28:27", "id": 3, - "modify_time": "2026-03-20T10:28:27.208007+08:00", + "modify_time": "2026-03-20 10:28:27", "name": "审核员", "password": "review123", "phone": "13800000003", @@ -302,6 +311,9 @@ "table": "admin" }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -312,7 +324,6 @@ "name": "table", "required": false, "type": "string", - "example": "admin", "in": "query" } ], @@ -338,52 +349,52 @@ "result": true }, { - "affected": 0, + "affected": 1, "name": "Exec 原生执行 (双引号)", "result": true }, { - "count": 18, + "count": 46, "name": "USER_TABLES 查询", "result": true }, { "columns": [ { - "COLUMN_NAME": "id", - "DATA_TYPE": "INT" + "column_name": "id", + "data_type": "INT" }, { - "COLUMN_NAME": "name", - "DATA_TYPE": "VARCHAR" + "column_name": "name", + "data_type": "VARCHAR" }, { - "COLUMN_NAME": "phone", - "DATA_TYPE": "VARCHAR" + "column_name": "phone", + "data_type": "VARCHAR" }, { - "COLUMN_NAME": "state", - "DATA_TYPE": "INT" + "column_name": "state", + "data_type": "INT" }, { - "COLUMN_NAME": "password", - "DATA_TYPE": "VARCHAR" + "column_name": "password", + "data_type": "VARCHAR" }, { - "COLUMN_NAME": "role_id", - "DATA_TYPE": "INT" + "column_name": "role_id", + "data_type": "INT" }, { - "COLUMN_NAME": "title", - "DATA_TYPE": "VARCHAR" + "column_name": "title", + "data_type": "VARCHAR" }, { - "COLUMN_NAME": "create_time", - "DATA_TYPE": "TIMESTAMP" + "column_name": "create_time", + "data_type": "TIMESTAMP" }, { - "COLUMN_NAME": "modify_time", - "DATA_TYPE": "TIMESTAMP" + "column_name": "modify_time", + "data_type": "TIMESTAMP" } ], "count": 9, @@ -393,6 +404,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -484,10 +498,125 @@ { "label": null, "name": "test_batch" + }, + { + "label": "应用表", + "name": "app" + }, + { + "label": "应用分类表", + "name": "app_category" + }, + { + "label": "应用凭证表", + "name": "app_credential" + }, + { + "label": "应用评分表", + "name": "app_rating" + }, + { + "label": "应用评分附件表", + "name": "app_rating_file" + }, + { + "label": "开发者表", + "name": "developer" + }, + { + "label": "意见反馈表", + "name": "feedback" + }, + { + "label": "反馈附件表", + "name": "feedback_file" + }, + { + "label": "帮助分类表", + "name": "help_category" + }, + { + "label": "帮助问题表", + "name": "help_question" + }, + { + "label": null, + "name": "hotime_cache" + }, + { + "label": "登录日志表", + "name": "login_log" + }, + { + "label": "消息主表", + "name": "message" + }, + { + "label": "消息回调日志表", + "name": "message_callback_log" + }, + { + "label": "新闻公告表", + "name": "news" + }, + { + "label": "新闻分类表", + "name": "news_category" + }, + { + "label": "OAuth授权码表", + "name": "oauth_code" + }, + { + "label": "组织部门表", + "name": "org" + }, + { + "label": "组织应用关联表", + "name": "org_app" + }, + { + "label": "职务岗位表", + "name": "position" + }, + { + "label": "角色表", + "name": "role" + }, + { + "label": "用户表", + "name": "user" + }, + { + "label": "用户应用关联表", + "name": "user_app" + }, + { + "label": "用户认证表", + "name": "user_auth" + }, + { + "label": "用户设备表", + "name": "user_device" + }, + { + "label": "用户钉钉关联表", + "name": "user_dingtalk" + }, + { + "label": "用户消息关联表", + "name": "user_message" + }, + { + "label": "用户组织关联表", + "name": "user_org" } ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -499,6 +628,439 @@ "summary": "查询达梦所有表", "tested": true }, + { + "cases": [ + { + "name": "result是布尔", + "method": "GET", + "passed": true, + "response": { + "result": true, + "status": 0 + }, + "expect": { + "result": true, + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/bool_result", + "project": "app", + "summary": "返回布尔", + "tested": true + }, + { + "cases": [ + { + "name": "名称为空-错误断言", + "method": "POST", + "passed": true, + "form": { + "name": "" + }, + "response": { + "error": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "result": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "status": 3 + }, + "expect": { + "error": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "result": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "status": 3 + } + }, + { + "name": "名称正确-结构校验", + "method": "POST", + "passed": true, + "form": { + "name": "测试" + }, + "response": { + "result": { + "created": true, + "id": 1, + "name": "测试" + }, + "status": 0 + }, + "expect": { + "result": { + "created": true, + "id": 1, + "name": "sample" + }, + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "POST", + "params": [ + { + "name": "name", + "required": false, + "type": "string", + "in": "form" + } + ], + "path": "/app/expect_demo/error_demo", + "project": "app", + "summary": "错误+成功混合", + "tested": true + }, + { + "cases": [ + { + "name": "result是小数", + "method": "GET", + "passed": true, + "response": { + "result": 3.14, + "status": 0 + }, + "expect": { + "result": 1, + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/float_result", + "project": "app", + "summary": "返回小数", + "tested": true + }, + { + "cases": [ + { + "name": "result是Map-校验字段名和类型", + "method": "GET", + "passed": true, + "response": { + "result": { + "id": 1, + "in_stock": true, + "name": "示例商品", + "price": 19.9 + }, + "status": 0 + }, + "expect": { + "result": { + "id": 1, + "in_stock": true, + "name": "sample", + "price": 1 + }, + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/map_result", + "project": "app", + "summary": "返回对象", + "tested": true + }, + { + "cases": [ + { + "name": "深层嵌套Map+Slice+Map", + "method": "GET", + "passed": true, + "response": { + "result": { + "customer": { + "id": 1, + "name": "张三", + "phone": "13800138000" + }, + "extra": { + "delivery": { + "address": "XX路1号", + "fee": 5 + } + }, + "id": 100, + "items": [ + { + "goods_name": "苹果", + "id": 1, + "price": 3.5, + "quantity": 5 + }, + { + "goods_name": "香蕉", + "id": 2, + "price": 2, + "quantity": 10 + } + ], + "title": "测试订单" + }, + "status": 0 + }, + "expect": { + "result": { + "customer": { + "id": 1, + "name": "sample", + "phone": "sample" + }, + "extra": { + "delivery": { + "address": "sample", + "fee": 1 + } + }, + "id": 1, + "items": [ + { + "goods_name": "sample", + "id": 1, + "price": 1, + "quantity": 1 + } + ], + "title": "sample" + }, + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/nested_result", + "project": "app", + "summary": "返回嵌套对象", + "tested": true + }, + { + "cases": [ + { + "name": "只校验status=0", + "method": "GET", + "passed": true, + "response": { + "result": { + "data": "无预期校验", + "id": 1 + }, + "status": 0 + }, + "expect": { + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/no_expect", + "project": "app", + "summary": "不传预期(仅校验status)", + "tested": true + }, + { + "cases": [ + { + "name": "result是整数", + "method": "GET", + "passed": true, + "response": { + "result": 42, + "status": 0 + }, + "expect": { + "result": 1, + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/number_result", + "project": "app", + "summary": "返回整数", + "tested": true + }, + { + "cases": [ + { + "name": "result直接是Slice", + "method": "GET", + "passed": true, + "response": { + "result": [ + { + "id": 1, + "name": "标签A" + }, + { + "id": 2, + "name": "标签B" + } + ], + "status": 0 + }, + "expect": { + "result": [ + { + "id": 1, + "name": "sample" + } + ], + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/slice_result", + "project": "app", + "summary": "返回数组", + "tested": true + }, + { + "cases": [ + { + "name": "result是字符串", + "method": "GET", + "passed": true, + "response": { + "result": "操作成功", + "status": 0 + }, + "expect": { + "result": "操作成功", + "status": 0 + } + } + ], + "ctr": "expect_demo", + "method": "GET", + "params": [], + "path": "/app/expect_demo/string_result", + "project": "app", + "summary": "返回字符串", + "tested": true + }, + { + "cases": [ + { + "name": "名称为空", + "method": "POST", + "passed": true, + "form": { + "name": "" + }, + "response": { + "error": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "result": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "status": 3 + }, + "expect": { + "error": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "result": { + "msg": "名称不能为空", + "type": "请求参数异常" + }, + "status": 3 + } + }, + { + "name": "写入并校验DB-正确", + "method": "POST", + "passed": true, + "form": { + "name": "verify_test" + }, + "response": { + "result": { + "id": 173, + "name": "verify_test" + }, + "status": 0 + }, + "expect": { + "result": { + "id": 1, + "name": "sample" + }, + "status": 0 + } + }, + { + "name": "写入并校验DB-故意失败", + "method": "POST", + "passed": false, + "form": { + "name": "verify_fail" + }, + "response": { + "result": { + "id": 174, + "name": "verify_fail" + }, + "status": 0 + }, + "expect": { + "result": { + "id": 1, + "name": "sample" + }, + "status": 0 + }, + "verifyError": "test_batch.state 期望 99, 实际 0(接口写入的是0,这里故意校验99来演示失败效果)" + } + ], + "ctr": "expect_demo", + "method": "POST", + "params": [ + { + "name": "name", + "required": false, + "type": "string", + "in": "form" + } + ], + "path": "/app/expect_demo/verify_demo", + "project": "app", + "summary": "Verify数据库校验", + "tested": true + }, { "cases": [ { @@ -514,6 +1076,9 @@ "skip": true }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -524,7 +1089,6 @@ "name": "table", "required": false, "type": "string", - "example": "admin", "in": "query" } ], @@ -545,6 +1109,9 @@ "skip": true }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -568,6 +1135,9 @@ "skip": true }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -591,12 +1161,12 @@ "success": true, "tests": [ { - "count": 11, + "count": 0, "name": "Count 总数统计", "result": true }, { - "count": 9, + "count": 0, "name": "Count 条件统计", "result": true }, @@ -604,23 +1174,23 @@ "lastQuery": "SELECT SUM(click_num) FROM \"article\" WHERE \"state\" =? ;", "name": "Sum 求和 (单字段名)", "result": true, - "sum": 12311 + "sum": 0 }, { - "avg": 1367.888889, + "avg": 0, "lastQuery": "SELECT AVG(click_num) FROM \"article\" WHERE \"state\" =? ;", "name": "Avg 平均值 (单字段名)", "result": true }, { "lastQuery": "SELECT MAX(click_num) FROM \"article\" WHERE \"state\" =? ;", - "max": 9999, + "max": 0, "name": "Max 最大值 (单字段名)", "result": true }, { "lastQuery": "SELECT MIN(sort) FROM \"article\" WHERE \"state\" =? ;", - "min": 1, + "min": 0, "name": "Min 最小值 (单字段名)", "result": true }, @@ -629,42 +1199,42 @@ "result": true, "stats": [ { - "article_count": 4, + "article_count": 36, "avg_clicks": "450", "ctg_id": 2, - "total_clicks": 1800 + "total_clicks": 16200 }, { - "article_count": 3, + "article_count": 27, "avg_clicks": "3461", "ctg_id": 1, - "total_clicks": 10383 + "total_clicks": 93447 }, { - "article_count": 1, + "article_count": 9, "avg_clicks": "32", "ctg_id": 3, - "total_clicks": 32 + "total_clicks": 288 }, { - "article_count": 1, + "article_count": 9, "avg_clicks": "96", "ctg_id": 4, - "total_clicks": 96 + "total_clicks": 864 } ] }, { "lastQuery": "SELECT SUM(\"article\".\"click_num\") FROM \"article\" WHERE \"state\" =? ;", - "match_single_field": false, + "match_single_field": true, "name": "Sum 求和 (table.column 格式)", "result": true, "sum": 0 }, { "name": "聚合函数一致性验证", - "result": false, - "summary": "Sum: 12311=0, Avg: 1367.888889=0, Max: 9999=0, Min: 1=0" + "result": true, + "summary": "Sum: 0=0, Avg: 0=0, Max: 0=0, Min: 0=0" }, { "lastQuery": "SELECT SUM(\"article\".\"click_num\") FROM \"article\" LEFT JOIN \"ctg\" ON \"article\".\"ctg_id\" = \"ctg\".\"id\" WHERE \"article\".\"state\" =? ;", @@ -673,7 +1243,7 @@ "sum": 0 }, { - "count": 9, + "count": 0, "lastQuery": "SELECT COUNT(*) FROM \"article\" LEFT JOIN \"ctg\" ON \"article\".\"ctg_id\" = \"ctg\".\"id\" WHERE \"article\".\"state\" =? ;", "name": "Count 带 JOIN", "result": true @@ -681,6 +1251,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -705,25 +1278,25 @@ "success": true, "tests": [ { - "adminId": 10, - "lastQuery": "INSERT INTO \"admin\" (\"name\",\"phone\",\"state\",\"password\",\"role_id\",\"title\",\"create_time\",\"modify_time\") VALUES (?,?,?,?,?,?,NOW(),NOW());", + "adminId": 88, + "lastQuery": "INSERT INTO \"admin\" (\"phone\",\"state\",\"password\",\"role_id\",\"title\",\"create_time\",\"modify_time\",\"name\") VALUES (?,?,?,?,?,NOW(),NOW(),?);", "name": "Insert 插入测试 (admin表)", "result": true }, { "admin": { - "create_time": "2026-03-20T10:58:29.604213+08:00", - "id": 10, - "modify_time": "2026-03-20T10:58:29.604215+08:00", - "name": "测试管理员_1773975510", + "create_time": "2026-03-21 13:33:57", + "id": 88, + "modify_time": "2026-03-21 13:33:57", + "name": "测试管理员_1774071237", "password": "test123456", - "phone": "13873975510", + "phone": "13874071237", "role_id": 1, "state": 1, "title": "测试职位" }, "name": "Get 获取单条记录测试", - "result": false + "result": true }, { "count": 4, @@ -774,7 +1347,7 @@ "result": true }, { - "count": 1, + "count": 3, "lastQuery": "SELECT id,title FROM \"article\" WHERE \"title\" LIKE ? LIMIT 3 ;", "name": "LIKE 模糊查询 ([~])", "result": true @@ -845,17 +1418,17 @@ "success": true, "tests": [ { - "count": 9, + "count": 81, "name": "基本链式查询 Table().Where().Select()", "result": true }, { - "count": 9, + "count": 81, "name": "链式 And 条件", "result": true }, { - "count": 9, + "count": 81, "name": "链式 Or 条件", "result": true }, @@ -879,7 +1452,7 @@ "result": true }, { - "count": 9, + "count": 0, "name": "链式 Count 统计", "result": true }, @@ -893,25 +1466,25 @@ "result": true, "stats": [ { - "cnt": 3, + "cnt": 27, "ctg_id": 1 }, { - "cnt": 4, + "cnt": 36, "ctg_id": 2 }, { - "cnt": 1, + "cnt": 9, "ctg_id": 3 }, { - "cnt": 1, + "cnt": 9, "ctg_id": 4 } ] }, { - "affected": 0, + "affected": 1, "name": "链式 Update 更新", "result": true } @@ -1008,12 +1581,12 @@ "success": true, "tests": [ { - "count": 11, + "count": 0, "name": "Count 总数统计", "result": true }, { - "count": 9, + "count": 0, "name": "Count 条件统计", "result": true }, @@ -1021,23 +1594,23 @@ "lastQuery": "SELECT SUM(click_num) FROM \"article\" WHERE \"state\" =? ;", "name": "Sum 求和 (单字段名)", "result": true, - "sum": 12311 + "sum": 0 }, { - "avg": 1367.888889, + "avg": 0, "lastQuery": "SELECT AVG(click_num) FROM \"article\" WHERE \"state\" =? ;", "name": "Avg 平均值 (单字段名)", "result": true }, { "lastQuery": "SELECT MAX(click_num) FROM \"article\" WHERE \"state\" =? ;", - "max": 9999, + "max": 0, "name": "Max 最大值 (单字段名)", "result": true }, { "lastQuery": "SELECT MIN(sort) FROM \"article\" WHERE \"state\" =? ;", - "min": 1, + "min": 0, "name": "Min 最小值 (单字段名)", "result": true }, @@ -1046,42 +1619,42 @@ "result": true, "stats": [ { - "article_count": 4, + "article_count": 36, "avg_clicks": "450", "ctg_id": 2, - "total_clicks": 1800 + "total_clicks": 16200 }, { - "article_count": 3, + "article_count": 27, "avg_clicks": "3461", "ctg_id": 1, - "total_clicks": 10383 + "total_clicks": 93447 }, { - "article_count": 1, + "article_count": 9, "avg_clicks": "32", "ctg_id": 3, - "total_clicks": 32 + "total_clicks": 288 }, { - "article_count": 1, + "article_count": 9, "avg_clicks": "96", "ctg_id": 4, - "total_clicks": 96 + "total_clicks": 864 } ] }, { "lastQuery": "SELECT SUM(\"article\".\"click_num\") FROM \"article\" WHERE \"state\" =? ;", - "match_single_field": false, + "match_single_field": true, "name": "Sum 求和 (table.column 格式)", "result": true, "sum": 0 }, { "name": "聚合函数一致性验证", - "result": false, - "summary": "Sum: 12311=0, Avg: 1367.888889=0, Max: 9999=0, Min: 1=0" + "result": true, + "summary": "Sum: 0=0, Avg: 0=0, Max: 0=0, Min: 0=0" }, { "lastQuery": "SELECT SUM(\"article\".\"click_num\") FROM \"article\" LEFT JOIN \"ctg\" ON \"article\".\"ctg_id\" = \"ctg\".\"id\" WHERE \"article\".\"state\" =? ;", @@ -1090,7 +1663,7 @@ "sum": 0 }, { - "count": 9, + "count": 0, "lastQuery": "SELECT COUNT(*) FROM \"article\" LEFT JOIN \"ctg\" ON \"article\".\"ctg_id\" = \"ctg\".\"id\" WHERE \"article\".\"state\" =? ;", "name": "Count 带 JOIN", "result": true @@ -1107,7 +1680,7 @@ "result": true }, { - "count": 4, + "count": 5, "name": "PageSelect 第二页", "result": true }, @@ -1147,7 +1720,7 @@ "tests": [ { "affected": 1, - "lastQuery": "MERGE INTO \"admin\" USING (SELECT ? AS \"title\", NOW() AS \"create_time\", NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\", ? AS \"state\", ? AS \"password\", ? AS \"role_id\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"title\", \"create_time\", \"modify_time\", \"name\", \"phone\", \"state\", \"password\", \"role_id\") VALUES (src.\"title\", src.\"create_time\", src.\"modify_time\", src.\"name\", src.\"phone\", src.\"state\", src.\"password\", src.\"role_id\")", + "lastQuery": "MERGE INTO \"admin\" USING (SELECT ? AS \"password\", ? AS \"role_id\", ? AS \"title\", NOW() AS \"create_time\", NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\", ? AS \"state\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"password\", \"role_id\", \"title\", \"create_time\", \"modify_time\", \"name\", \"phone\", \"state\") VALUES (src.\"password\", src.\"role_id\", src.\"title\", src.\"create_time\", src.\"modify_time\", src.\"name\", src.\"phone\", src.\"state\")", "name": "Upsert 插入新记录 (admin表)", "result": true }, @@ -1156,12 +1729,12 @@ "name": "Upsert 更新已存在记录", "result": true, "updatedAdmin": { - "create_time": "2026-03-20T10:58:33.501828+08:00", - "id": 11, - "modify_time": "2026-03-20T10:58:33.566436+08:00", + "create_time": "2026-03-21 13:34:05", + "id": 89, + "modify_time": "2026-03-21 13:34:05", "name": "Upsert更新后管理员", "password": "test123", - "phone": "19973975514", + "phone": "19974071244", "role_id": 2, "state": 1, "title": "更新后职位" @@ -1187,6 +1760,9 @@ } }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1223,6 +1799,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1246,17 +1825,17 @@ "success": true, "tests": [ { - "count": 9, + "count": 81, "name": "基本链式查询 Table().Where().Select()", "result": true }, { - "count": 9, + "count": 81, "name": "链式 And 条件", "result": true }, { - "count": 9, + "count": 81, "name": "链式 Or 条件", "result": true }, @@ -1280,7 +1859,7 @@ "result": true }, { - "count": 9, + "count": 0, "name": "链式 Count 统计", "result": true }, @@ -1294,31 +1873,34 @@ "result": true, "stats": [ { - "cnt": 3, + "cnt": 27, "ctg_id": 1 }, { - "cnt": 4, + "cnt": 36, "ctg_id": 2 }, { - "cnt": 1, + "cnt": 9, "ctg_id": 3 }, { - "cnt": 1, + "cnt": 9, "ctg_id": 4 } ] }, { - "affected": 0, + "affected": 1, "name": "链式 Update 更新", "result": true } ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1363,7 +1945,7 @@ "result": true }, { - "count": 1, + "count": 3, "lastQuery": "SELECT id,title FROM \"article\" WHERE \"title\" LIKE ? LIMIT 3 ;", "name": "LIKE 模糊查询 ([~])", "result": true @@ -1430,6 +2012,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1453,25 +2038,25 @@ "success": true, "tests": [ { - "adminId": 9, - "lastQuery": "INSERT INTO \"admin\" (\"password\",\"role_id\",\"title\",\"create_time\",\"modify_time\",\"name\",\"phone\",\"state\") VALUES (?,?,?,NOW(),NOW(),?,?,?);", + "adminId": 90, + "lastQuery": "INSERT INTO \"admin\" (\"modify_time\",\"name\",\"phone\",\"state\",\"password\",\"role_id\",\"title\",\"create_time\") VALUES (NOW(),?,?,?,?,?,?,NOW());", "name": "Insert 插入测试 (admin表)", "result": true }, { "admin": { - "create_time": "2026-03-20T10:58:25.844257+08:00", - "id": 9, - "modify_time": "2026-03-20T10:58:25.844258+08:00", - "name": "测试管理员_1773975506", + "create_time": "2026-03-21 13:34:06", + "id": 90, + "modify_time": "2026-03-21 13:34:06", + "name": "测试管理员_1774071246", "password": "test123456", - "phone": "13873975506", + "phone": "13874071246", "role_id": 1, "state": 1, "title": "测试职位" }, "name": "Get 获取单条记录测试", - "result": false + "result": true }, { "count": 4, @@ -1497,6 +2082,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1602,6 +2190,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1630,7 +2221,7 @@ "result": true }, { - "count": 4, + "count": 5, "name": "PageSelect 第二页", "result": true }, @@ -1648,6 +2239,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1693,6 +2287,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], @@ -1717,7 +2314,7 @@ "tests": [ { "affected": 1, - "lastQuery": "MERGE INTO \"admin\" USING (SELECT NOW() AS \"create_time\", NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\", ? AS \"state\", ? AS \"password\", ? AS \"role_id\", ? AS \"title\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"create_time\", \"modify_time\", \"name\", \"phone\", \"state\", \"password\", \"role_id\", \"title\") VALUES (src.\"create_time\", src.\"modify_time\", src.\"name\", src.\"phone\", src.\"state\", src.\"password\", src.\"role_id\", src.\"title\")", + "lastQuery": "MERGE INTO \"admin\" USING (SELECT ? AS \"state\", ? AS \"password\", ? AS \"role_id\", ? AS \"title\", NOW() AS \"create_time\", NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"state\", \"password\", \"role_id\", \"title\", \"create_time\", \"modify_time\", \"name\", \"phone\") VALUES (src.\"state\", src.\"password\", src.\"role_id\", src.\"title\", src.\"create_time\", src.\"modify_time\", src.\"name\", src.\"phone\")", "name": "Upsert 插入新记录 (admin表)", "result": true }, @@ -1726,12 +2323,12 @@ "name": "Upsert 更新已存在记录", "result": true, "updatedAdmin": { - "create_time": "2026-03-20T10:58:25.400239+08:00", - "id": 8, - "modify_time": "2026-03-20T10:58:25.461693+08:00", + "create_time": "2026-03-21 13:34:09", + "id": 91, + "modify_time": "2026-03-21 13:34:10", "name": "Upsert更新后管理员", "password": "test123", - "phone": "19973975506", + "phone": "19974071249", "role_id": 2, "state": 1, "title": "更新后职位" @@ -1740,6 +2337,9 @@ ] }, "status": 0 + }, + "expect": { + "status": 0 } } ], diff --git a/example/tpt/swagger/app/index.html b/example/tpt/swagger/app/index.html index 72d278d..ccc1331 100644 --- a/example/tpt/swagger/app/index.html +++ b/example/tpt/swagger/app/index.html @@ -53,6 +53,20 @@ input,select,textarea,button{font:inherit}pre{margin:0} .csc h5{color:var(--accent);margin:0 0 4px;font-size:11px;letter-spacing:.5px;display:flex;align-items:center} .csc h5 .cp-sm{margin-left:auto}.csc h5:not(:first-child){margin-top:8px} pre.j{background:#0a0a1a;padding:6px 8px;border-radius:var(--r);font-size:12px;line-height:1.4;color:#aaa;white-space:pre-wrap;word-break:break-all;max-height:260px;overflow:auto;font-family:Consolas,Monaco,monospace} +.resp-wrap{display:flex;gap:0}.resp-main{flex:1;min-width:0} +.resp-main .rsp-hd{display:flex;align-items:center;margin-bottom:3px;font-size:10px;color:var(--fg2);letter-spacing:.5px} +.resp-main .rsp-hd .cp-sm{margin-left:auto} +.exp-col-btn{flex-shrink:0;width:22px;display:flex;align-items:stretch;justify-content:center;margin:0 2px} +.exp-col-btn button{writing-mode:vertical-rl;background:var(--accent);border:none;color:#fff;font-size:10px;padding:6px 3px;border-radius:var(--r);cursor:pointer;user-select:none;line-height:1.2;letter-spacing:1px;opacity:.85} +.exp-col-btn button:hover{opacity:1} +.exp-panel{flex:1;min-width:0;display:none;padding-left:6px} +.resp-wrap.exp-on .exp-panel{display:block} +.exp-panel .ep-hd{display:flex;align-items:center;margin-bottom:3px;font-size:10px;color:var(--fg2);letter-spacing:.5px} +.exp-panel .ep-hd .cp-sm{margin-left:auto} +.exp-panel pre.j{border-left:2px solid var(--accent)} +pre.j.resp-j{max-height:calc(100vh - 280px);min-height:120px} +.verify-err{margin-top:8px;padding:8px 10px;background:rgba(255,77,79,0.08);border:1px solid rgba(255,77,79,0.3);border-radius:var(--r);font-size:12px;color:#ff6b6b;line-height:1.5;white-space:pre-wrap;word-break:break-all} +.verify-label{display:inline-block;background:var(--red);color:#fff;font-size:10px;padding:1px 6px;border-radius:var(--r);margin-right:8px;vertical-align:middle} .sec{margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}.sec-title{color:var(--fg2);font-size:11px;letter-spacing:.5px;margin-bottom:4px} .row-hd{display:flex;align-items:center;margin-bottom:4px;padding:4px 8px;background:var(--bg2);border-radius:var(--r)}.row-hd .sec-title{flex:1;margin:0} .kv{display:flex;gap:4px;margin-bottom:3px;align-items:center}.kv input{flex:1;padding:4px 6px;border:1px solid var(--border);border-radius:var(--r);background:var(--bg2);color:var(--fg);font-size:12px;outline:none} @@ -187,12 +201,18 @@ function cases(e){ h+='
'+fj(c.response)+''; - else h+='无响应'; + const hasDetail=c.expect&&c.expect.result!==undefined;const expId='ce'+i; + h+='
'+fj(c.response)+'';else h+='无响应';h+='
'+fj(c.expect)+'
'+fj(c.response)+'';else h+='无响应';} h+='
'+fj(c.response)+''; - else h+='无响应'; + const hasDetail=c.expect&&c.expect.result!==undefined; + h+='
'+fj(c.response)+'';else h+='无响应';h+='
'+fj(c.expect)+'
'+fj(c.response)+'';else h+='无响应';} h+='
'+fj(c.response)+''; - else h+='无响应'; + const hasDetail=c.expect&&c.expect.result!==undefined;const expId='ce'+i; + h+='
'+fj(c.response)+'';else h+='无响应';h+='
'+fj(c.expect)+'
'+fj(c.response)+'';else h+='无响应';} h+='
'+fj(c.response)+''; - else h+='无响应'; + const hasDetail=c.expect&&c.expect.result!==undefined; + h+='
'+fj(c.response)+'';else h+='无响应';h+='
'+fj(c.expect)+'
'+fj(c.response)+'';else h+='无响应';} h+='