feat(swagger): 增强 Swagger 生成逻辑与参数推断
- 新增参数推断功能,自动从测试用例中推断 API 参数的必填性、类型和示例值 - 更新 GenerateSwagger 方法,支持在生成的 API 规范中包含推断的参数信息 - 优化文档,增加对新功能的说明,包括参数推断和覆盖率字段的详细描述 - 改进调试控制台的结构,支持模块化输出和独立导航页生成 - 增强并发保护与缓存隔离机制,确保测试环境的稳定性
This commit is contained in:
+156
-55
@@ -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,生产代码路径不受任何影响。
|
||||
|
||||
Reference in New Issue
Block a user