feat(swagger): 增强 Swagger 生成逻辑与参数推断

- 新增参数推断功能,自动从测试用例中推断 API 参数的必填性、类型和示例值
- 更新 GenerateSwagger 方法,支持在生成的 API 规范中包含推断的参数信息
- 优化文档,增加对新功能的说明,包括参数推断和覆盖率字段的详细描述
- 改进调试控制台的结构,支持模块化输出和独立导航页生成
- 增强并发保护与缓存隔离机制,确保测试环境的稳定性
This commit is contained in:
2026-03-15 10:36:49 +08:00
parent 87098a9180
commit 71e415dfe1
2 changed files with 549 additions and 176 deletions
+156 -55
View File
@@ -11,8 +11,11 @@
- [完整场景对照](#完整场景对照)
- [事务隔离机制](#事务隔离机制)
- [覆盖率报告](#覆盖率报告)
- [api-spec.json 覆盖率字段说明](#api-specjson-覆盖率字段说明)
- [params 参数推断说明](#api-specjson-覆盖率字段说明)
- [API 调试控制台](#api-调试控制台)
- [指定运行范围](#指定运行范围)
- [并发保护与缓存隔离](#并发保护与缓存隔离)
- [框架改动说明](#框架改动说明)
---
@@ -26,9 +29,11 @@ HoTime 内置 API 测试框架,核心能力:
| **零启动** | 基于 `net/http/httptest`,不需要启动 HTTP 服务器 |
| **事务回滚** | 每个接口方法独立开启数据库事务,测试结束自动回滚,数据库不留任何痕迹 |
| **SAVEPOINT** | 业务代码中的 `that.Db.Action()` 在测试模式下自动用 SAVEPOINT 替代真事务,嵌套事务完整可用 |
| **并发保护** | `testMu` 互斥锁自动保护 `testTx` 单连接,业务代码中的 `go func()` 协程不会导致 `busy buffer` |
| **缓存隔离** | 测试启动时自动禁用 DB/Redis 缓存,避免缓存操作与测试事务锁冲突 |
| **链式 API** | `a.JSON(...).Post("描述", 期望status)` 一行完成:数据组装 + 请求 + 断言 |
| **覆盖率** | 自动对比路由注册表与测试定义,输出哪些接口已覆盖、哪些未覆盖 |
| **调试控制台** | 根据测试用例自动生成交互式 API 调试控制台,支持在线测试、认证管理、参数编辑 |
| **调试控制台** | 按模块生成独立的交互式 API 调试控制台,支持在线测试、认证管理、参数编辑 |
---
@@ -345,20 +350,6 @@ that.Db.Action(func(db HoTimeDB) bool {
| 数据持久化 | 持久化到数据库 | 测试结束全部回滚 |
| 对现有代码影响 | 无 | 无(`testTx` 默认 nil |
### 一次性数据的处理
对于需要随机数据的测试(如注册),直接用 `RandX` 生成随机值即可,**无需手动清理**:
```go
// app/user_test.go 中的 UserTest 片段
"create": {Desc: "用户注册", Func: func(a *Api) {
// 生成随机手机号,测试完成后事务自动回滚,数据库中不留任何记录
phone := "138" + ObjToStr(RandX(10000000, 99999999))
a.JSON(Map{"phone": phone, "password": "123456"}).Post("正常注册", 0)
a.JSON(Map{"phone": phone, "password": "123456"}).Post("重复注册", 3, "该手机号已被注册")
}},
```
---
## 覆盖率报告
@@ -381,26 +372,86 @@ func TestMain(m *testing.M) {
总接口: 42 | 已覆盖: 6 | 覆盖率: 14.3%
已覆盖的接口:
+ /app/user/login 4 用例 (4 通过, 0 失败) 12ms
+ /app/user/info 2 用例 (2 通过, 0 失败) 5ms
+ /app/user/token 2 用例 (2 通过, 0 失败) 4ms
+ /app/user/create 4 用例 (4 通过, 0 失败) 18ms
+ /app/user/forget 2 用例 (2 通过, 0 失败) 6ms
+ /app/user/file 2 用例 (2 通过, 0 失败) 9ms
+ /api/user/login 4 用例 (4 通过, 0 失败) 12ms
+ /api/user/info 2 用例 (2 通过, 0 失败) 5ms
+ /api/user/token 2 用例 (2 通过, 0 失败) 4ms
+ /api/user/create 4 用例 (4 通过, 0 失败) 18ms
+ /api/user/forget 2 用例 (2 通过, 0 失败) 6ms
+ /api/user/file 2 用例 (2 通过, 0 失败) 9ms
未覆盖的接口:
- app/goods/list
- app/goods/create
- app/order/list
- api/goods/list
- api/goods/create
- api/order/list
- ... (共36个未覆盖)
========================================
```
### api-spec.json 覆盖率字段说明
`GenerateSwagger` 生成的每个 `api-spec.json` 文件中,每条 `endpoint` 记录包含以下与测试状态相关的字段:
```json
{
"path": "/api/user/login",
"project": "api",
"ctr": "user",
"method": "POST",
"summary": "用户登录",
"tested": true,
"params": [
{ "name": "name", "required": true, "type": "string", "example": "13800138000", "in": "json" },
{ "name": "password", "required": false, "type": "string", "example": "123456", "in": "json" }
],
"cases": [
{
"name": "正常登录",
"method": "POST",
"passed": true,
"query": {},
"json": { "name": "13800138000", "password": "123456" },
"response": { "status": 0, "msg": "ok", "data": {} }
},
{
"name": "密码为空",
"method": "POST",
"passed": true,
"json": { "name": "13800138000", "password": "" },
"response": { "status": 4, "msg": "用户名或密码不能为空" }
}
]
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `tested` | bool | 是否在 `CtrTest` 中定义了测试,`false` = 未测试 |
| `params` | array | 从测试用例自动推断的参数规格列表 |
| `params[].name` | string | 参数名称 |
| `params[].required` | bool | 是否必填(判断依据:该参数出现且值为空、同时 response.status==3 的用例存在) |
| `params[].type` | string | 推断的参数类型(`string` / `number` / `boolean` / `array` / `object` |
| `params[].example` | any | 取第一个 status==0 成功用例中该参数的值作为示例 |
| `params[].in` | string | 参数位置:`query` / `form` / `json` |
| `cases` | array | 实际运行的测试用例列表;若 `tested=false` 则为空数组 |
| `cases[].passed` | bool | 该用例是否通过(断言的 status/message 均匹配) |
| `cases[].query` | object | 该用例的 Query 参数(GET 场景) |
| `cases[].json` | object | 该用例的 JSON Body`a.JSON(...)` 场景) |
| `cases[].form` | object | 该用例的 Form 参数(`a.Form(...)` 场景) |
| `cases[].response` | object | 该用例的实际响应体 |
**侧边栏徽章规则**(调试控制台):
| 显示 | 含义 |
|------|------|
| `未测试`(灰色) | `tested=false`,该接口没有任何测试用例 |
| `N/N`(绿色) | 所有用例通过,如 `4/4` |
| `N/N`(红色) | 存在失败用例,如 `2/4` |
---
## API 调试控制台
`GenerateSwagger` 遍历 `TestProj` 中所有项目的路由和测试用例,生成一个**交互式 API 调试控制台**(暗色高对比度主题),支持在线调试、参数编辑、认证管理
`GenerateSwagger` 按模块生成独立的交互式 API 调试控制台(暗色高对比度主题),每个模块一个子目录,支持独立分发
```go
testApp.GenerateSwagger(
@@ -410,47 +461,56 @@ testApp.GenerateSwagger(
)
```
生成文件:
### 生成目录结构
每个模块(`TestProj` 中的项目名)生成独立子目录,同时自动生成根导航页:
```
{outputDir}/swagger/
├── api-spec.json ← 自定义 API 规范文件(包含路由、测试用例、请求/响应数据
── index.html ← 交互式调试控制台
├── index.html ← 根导航页(API 文档中心,卡片式链接到各模块
── api/
│ ├── api-spec.json ← api 模块的 API 规范(路由、测试用例、请求/响应数据)
│ └── index.html ← api 模块的调试控制台
├── admin/
│ ├── api-spec.json
│ └── index.html
└── portal/
├── api-spec.json
└── index.html
```
访问地址:`http://localhost:8081/swagger/`
访问地址:`http://localhost:8081/swagger/`(根导航页),`http://localhost:8081/swagger/api/`api 模块)
> **独立分发**:每个模块目录可以单独拷贝和部署,不依赖其他模块。根导航页自动扫描子目录生成链接。
### 控制台功能
| 功能 | 说明 |
|------|------|
| **三级导航** | 左侧侧边栏按 项目 > 控制器 > 方法 三级树形展 |
| **搜索和筛选** | 支持关键字搜索,按 全部/已测试/未测试 筛选 |
| **测试用例查看** | 每个接口的测试用例以手风琴形式展示(第一个默认展开),包含请求头、请求参数(Query/JSON/Form/文件)、响应结果 |
| **接口调试** | 可编辑的请求头、Query 参数、文件上传、请求体(表单/JSON 互斥切换),直接发送请求 |
| **预填用例** | 下拉选择测试用例一键填充请求参数,方便快速调试 |
| **三级导航** | 左侧侧边栏按 项目 > 控制器 > 方法 三级树形展示(一级默认展开,二级默认收起) |
| **搜索和筛选** | 支持关键字搜索,按 全部/已测试/未测试/通过/未通过 五种模式筛选 |
| **左右分栏** | 接口信息栏右侧显示用例数量(`用例: N`);左侧为请求构建器,右侧为结果面板(两 tab 切换) |
| **测试用例 tab** | 右侧默认展示"测试用例"tab,手风琴列表(第一个默认展开);必填参数前显示红色 `*` 标记 |
| **发送结果 tab** | 点击"发送"后自动切换到"发送结果"tab 展示 cURL 命令和响应内容;可随时手动切回 |
| **必填参数推断** | 从测试用例自动推断必填参数(有 status==3 且值为空的用例),调试面板和用例展示均标记 `*` |
| **预填用例** | 默认选中第一个用例自动填充左侧所有参数;"手动填写"置于列表末尾;必填字段有 `*` 标记 |
| **全局认证** | 顶部认证栏支持四种模式:无认证、Header (Authorization)、URL (?token=)、Cookie,修改后实时同步到各参数区域 |
| **Cookie 隔离** | Header/URL 认证模式下自动阻止浏览器发送 Cookie(`credentials: 'omit'`),避免意外自动授权 |
| **cURL 生成** | 每次请求自动生成对应的 cURL 命令,支持一键复制 |
| **响应格式化** | JSON 响应自动格式化显示,支持复制 |
| **覆盖率可视化** | 已测试接口显示通过/失败计数徽章,未测试接口标记"未测试" |
### 多项目支持
所有注册项目的路由和测试用例合并到同一份 `api-spec.json`,在侧边栏按项目名分组,不会互相覆盖:
### 侧边栏结构
```
侧边栏结构:
├── app(项目)
│ ├── user(控制器)
│ ├── POST login 用户登录 ✅ 4/4
│ │ ├── POST info 未测试
│ └── POST create 用户注册 ✅ 4/4
│ └── goods(控制器)
└── ...
└── customer(项目)
└── pay(控制器)
└── ...
├── user(控制器,默认展开) ← 一级目录
│ ├── POST login 用户登录 ✅ 4/4
│ ├── POST info 未测试
── POST create 用户注册 ✅ 4/4
├── goods(控制器,点击展开) ← 二级目录
│ └── ...
└── order
└── ...
```
---
@@ -492,24 +552,65 @@ TestApi
---
## 并发保护与缓存隔离
### testMu 互斥锁
`testTx` 是单个 `*sql.Tx` 连接,MySQL 驱动不允许在同一连接上并发执行查询。业务代码中的 `go func()` 协程(如异步同步、统计任务)会尝试并发访问 `testTx`,导致 `busy buffer` 错误。
框架通过 `testMu *sync.Mutex` 自动序列化所有 `testTx` 上的操作:
| 操作 | 保护范围 |
|------|----------|
| `testTx.Query()` | 加锁 → 执行查询 → 消费结果集 → 解锁 |
| `testTx.Exec()` | 加锁 → 执行 → 解锁 |
| SAVEPOINT 操作 | 加锁 → 执行 → 解锁 |
生产环境不受影响(`testMu` 默认 nil,所有锁检查都有 nil 保护)。
### 缓存隔离
`NewTestApp` 启动时自动调用 `HoTimeCache.DisableDbCache()`,禁用 DB 和 Redis 缓存层,仅保留 Memory 缓存:
- 避免缓存的 `DELETE FROM cached` 操作与测试事务产生锁冲突(`Lock wait timeout`
- Session 通过 `WithSession()` 直接注入 Memory 缓存,不依赖 DB/Redis
- 生产环境不受影响(仅在 `NewTestApp` 中调用)
### 一次性数据的处理
对于需要随机数据的测试(如注册),直接用 `RandX` 生成随机值即可,**无需手动清理**:
```go
"create": {Desc: "用户注册", Func: func(a *Api) {
phone := "138" + ObjToStr(RandX(10000000, 99999999))
a.JSON(Map{"phone": phone, "password": "123456"}).Post("正常注册", 0)
a.JSON(Map{"phone": phone, "password": "123456"}).Post("重复注册", 3, "该手机号已被注册")
}},
```
---
## 框架改动说明
测试框架对 `hotimev1.5` 共改动 6 个文件,其中 3 个为修改、3 个为新增:
测试框架对 `hotimev1.5` 共改动 9 个文件,其中 6 个为修改、3 个为新增:
### 修改的文件
| 文件 | 改动 | 生产影响 |
|------|------|----------|
| `db/db.go` | 新增 `testTx *sql.Tx` 字段 + `BeginTestTx()`/`RollbackTestTx()` 方法 | 无(`testTx` 默认 nil |
| `db/query.go` | `Query``Exec` 各增加一个 `if testTx != nil` 分支 | 无(分支默认跳过) |
| `db/transaction.go` | `Action()` 增加 SAVEPOINT 分支 | 无(`testTx` 为 nil 时走原有逻辑) |
| `db/db.go` | 新增 `testTx *sql.Tx` + `testMu *sync.Mutex` 字段,`BeginTestTx()`(初始化互斥锁)/ `RollbackTestTx()` 方法 | 无(`testTx` 默认 nil |
| `db/query.go` | `Query``Exec` 各增加 `if testTx != nil` 分支 + `testMu` 互斥锁保护 + 跳过重试逻辑 | 无(分支默认跳过) |
| `db/transaction.go` | `Action()` 增加 SAVEPOINT 分支 + `testTx`/`testMu` 传递 + SAVEPOINT 操作加锁 | 无(`testTx` 为 nil 时走原有逻辑) |
| `db/crud.go` | `Select``testTx` 激活时跳过缓存逻辑 | 无(分支默认跳过) |
| `cache/cache.go` | 新增 `DisableDbCache()` 方法 + 新增 `SessionsGet`/`SessionsSet`/`SessionsDelete` 批量操作 | 无(方法不主动调用) |
| `session.go` | 对接批量 Session 缓存操作 | 无 |
### 新增的文件
| 文件 | 内容 |
|------|------|
| `testing_helper.go` | `TestApp``NewTestApp``SetupForTest``RunTests`(事务隔离)、`PrintCoverage`(覆盖率) |
| `testing_helper.go` | `TestApp``NewTestApp`(含 `DisableDbCache` 调用)`SetupForTest``RunTests`(事务隔离)、`PrintCoverage`(覆盖率) |
| `testing_api.go` | 注册类型 + `Api`/`ApiCase`/`ApiResponse` 链式 API + `TestCollector` |
| `testing_swagger.go` | `GenerateSwagger`合并所有项目生成 API 调试控制台(`api-spec.json` + `index.html` |
| `testing_swagger.go` | `GenerateSwagger`按模块生成子目录 + 根导航页 + 交互式调试控制台 |
> 所有新增文件仅在 `go test` 环境下被使用,生产构建不引入 `testing` 包依赖,完全不影响线上服务。
> 所有新增文件仅在 `go test` 环境下被使用,生产构建不引入 `testing` 包依赖,完全不影响线上服务。所有修改的文件都通过 nil 检查保护,`testTx`/`testMu` 默认为 nil,生产代码路径不受任何影响。