feat(testing): Flows/FromCase、增量 swagger 与 Doc-Driven 门禁
补齐多接口流程联调与控制台体验,并落地仓内 Doc-Driven+TDD 总规则与文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-2
@@ -1,5 +1,7 @@
|
||||
# HoTime 文档索引
|
||||
|
||||
本仓采用 **Doc-Driven + TDD**:行为一变必须回写 docs;改接口必须用 `-run` 精确测试验证(见 [Testing_API测试框架.md](Testing_API测试框架.md) 文首铁律与仓规则 `hotime-doc-tdd`)。
|
||||
|
||||
推荐按路径阅读,避免同一概念在多份文档里来回找。
|
||||
|
||||
## 阅读路径
|
||||
@@ -8,7 +10,7 @@
|
||||
2. **类型与工具** → [Common_工具类.md](Common_工具类.md)
|
||||
3. **数据库 ORM** → [HoTimeDB_使用说明.md](HoTimeDB_使用说明.md)(教程)→ [HoTimeDB_API参考.md](HoTimeDB_API参考.md)(速查)
|
||||
4. **开表 / 代码生成** → [DatabaseDesign_数据库设计规范.md](DatabaseDesign_数据库设计规范.md)(前置)→ [CodeGen_代码生成.md](CodeGen_代码生成.md)
|
||||
5. **接口测试** → [Testing_API测试框架.md](Testing_API测试框架.md)(日常用 `-run` 单测;全量见文末)
|
||||
5. **接口测试** → [Testing_API测试框架.md](Testing_API测试框架.md)(日常用 `-run` 单测;局限见文末;全量见文末)
|
||||
6. **运维** → [GracefulShutdown_优雅停机.md](GracefulShutdown_优雅停机.md)、[Seq_日志集成.md](Seq_日志集成.md)
|
||||
7. **规划** → [ROADMAP_改进规划.md](ROADMAP_改进规划.md)
|
||||
|
||||
@@ -22,7 +24,7 @@
|
||||
| HoTimeDB_API参考 | ORM 方法与条件语法速查 |
|
||||
| DatabaseDesign_数据库设计规范 | 表/字段/COMMENT 规范(CodeGen 输入契约) |
|
||||
| CodeGen_代码生成 | 生成器用法 + codeConfig / rule 配置 |
|
||||
| Testing_API测试框架 | API 测试;优先 `-run`,全量沉底 |
|
||||
| Testing_API测试框架 | API 测试、Flows、控制台;优先 `-run`;已知局限 |
|
||||
| GracefulShutdown_优雅停机 | 优雅停机与滚动重启 |
|
||||
| Seq_日志集成 | Seq 结构化日志 |
|
||||
| ROADMAP_改进规划 | 待改进项 |
|
||||
|
||||
@@ -266,6 +266,7 @@ a.JSON(Map{"phone": "138..."}).Post("正常注册", 0, Map{"id": notEmpty}) //
|
||||
|--------|--------|------|
|
||||
| 高 | 备注语法优化(`\|` 分隔符) | 待设计 |
|
||||
| 高 | 测试框架:响应字段部分匹配断言 | 待实现 |
|
||||
| 高 | 测试框架其它缺口(DB 断言、FromCase 无模板失败、Header API 等) | 见 [Testing 已知局限](Testing_API测试框架.md#已知局限与后续) |
|
||||
| 中 | 软删除/日志表支持 | 待设计 |
|
||||
| 低 | 多对多关联表增强 | 待评估 |
|
||||
| 低 | 自动填充字段扩展 | 待评估 |
|
||||
@@ -276,6 +277,7 @@ a.JSON(Map{"phone": "138..."}).Post("正常注册", 0, Map{"id": notEmpty}) //
|
||||
|
||||
| 日期 | 内容 |
|
||||
|------|------|
|
||||
| 2026-07-13 | 测试框架文档补齐 Flows/控制台;缺口清单链到 Testing |
|
||||
| 2026-01-24 | 初始版本,记录分析结果和待改进项 |
|
||||
| 2026-03-15 | 新增测试框架响应字段断言改进规划(七) |
|
||||
| 2026-03-20 | 完成达梦(DM8)数据库完整支持:Dialect 适配(MERGE INTO Upsert、双引号标识符)、LastInsertId、保留字自动引号、schema 正确处理、代码生成器 DM 兼容、缓存表自动建表、测试框架 DM 适配 |
|
||||
|
||||
+132
-59
@@ -14,18 +14,16 @@
|
||||
- [快速开始](#快速开始)
|
||||
- [运行测试(推荐:-run 单测)](#运行测试推荐-run-单测)
|
||||
- [用例编写范式](#用例编写范式)
|
||||
- [错误用例编写规范](#1-错误用例编写规范)
|
||||
- [测试数据准备](#2-测试数据准备)
|
||||
- [正确请求与响应结构校验](#3-正确请求与响应结构校验)
|
||||
- [Verify 统一校验](#4-verify-统一校验)
|
||||
- [简写支持说明](#简写支持说明)
|
||||
- [核心类型](#核心类型)
|
||||
- [业务流程联调(Flows)](#业务流程联调flows)
|
||||
- [优雅停测](#优雅停测)
|
||||
- [链式 API 参考](#链式-api-参考)
|
||||
- [事务隔离机制](#事务隔离机制)
|
||||
- [API 调试控制台](#api-调试控制台)
|
||||
- [并发保护与缓存隔离](#并发保护与缓存隔离)
|
||||
- [二进制与非 JSON 响应校验](#二进制与非-json-响应校验)
|
||||
- [全量回归与覆盖率(仅 CI / 发版前)](#全量回归与覆盖率仅-ci--发版前)
|
||||
- [已知局限与后续](#已知局限与后续)
|
||||
|
||||
---
|
||||
|
||||
@@ -41,8 +39,10 @@ HoTime 内置 API 测试框架,核心能力:
|
||||
| **并发保护** | `testMu` 互斥锁自动保护 `testTx` 单连接,业务代码中的 `go func()` 协程不会导致 `busy buffer` |
|
||||
| **缓存隔离** | 测试启动时自动禁用 DB/Redis 缓存,避免缓存操作与测试事务锁冲突 |
|
||||
| **链式 API** | `a.JSON(...).Post("描述", 期望status)` 一行完成:数据组装 + 请求 + 断言 |
|
||||
| **覆盖率** | 自动对比路由注册表与测试定义,输出哪些接口已覆盖、哪些未覆盖 |
|
||||
| **调试控制台** | 按模块生成独立的交互式 API 调试控制台,支持在线测试、认证管理、参数编辑 |
|
||||
| **Flows** | 多接口业务流程联调;`Group`/`Desc`/`Prep`;`FromCase` 绑定单接口用例并回写 |
|
||||
| **覆盖率** | 自动对比路由注册表与测试定义(**不含 Flow 步骤**,只统计单接口 path) |
|
||||
| **调试控制台** | 增量写入 swagger;流程分组 UI;发送结果 cURL 实时同步左侧参数 |
|
||||
| **优雅停测** | SIGINT/SIGTERM / `RequestStop`:停收新测、跑完当前、回滚事务、保留已落盘文档 |
|
||||
|
||||
---
|
||||
|
||||
@@ -184,15 +184,15 @@ var ProjectTest = ProjTest{
|
||||
var testApp *TestApp
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Chdir("..") // 确保工作目录为项目根目录
|
||||
testApp = NewTestApp("config/config.json", TestProj{
|
||||
"app": {Proj: Project, Tests: ProjectTest},
|
||||
})
|
||||
code := m.Run()
|
||||
// 以下两行适合 CI / 全量回归;日常用 -run 单测时也会执行,但覆盖率主要在全量时有意义
|
||||
testApp.PrintCoverage()
|
||||
testApp.GenerateSwagger("My API", "1.0.0", testApp.Config.GetString("tpt"))
|
||||
os.Exit(code)
|
||||
testApp = Test("config/config.json").
|
||||
WorkDir("..").
|
||||
Swagger("My API", "1.0.0").
|
||||
Proj(TestProj{
|
||||
"app": {Proj: Project, Tests: ProjectTest},
|
||||
})
|
||||
// 可选:.Flows(FlowTest{ "checkout": {Desc: "下单支付跑通", Func: checkoutFlow} })
|
||||
|
||||
os.Exit(testApp.Run(m))
|
||||
}
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
@@ -200,6 +200,8 @@ func TestApi(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
`Run(m)` 内部会:注册优雅停测信号 → `m.Run()` → 覆盖率 → Swagger 收尾(接口级增量写入)。
|
||||
`WorkDir` / `Swagger` / `Flows` 均可省略;不写 `Swagger` 则不生成调试控制台。
|
||||
### 推荐目录结构
|
||||
|
||||
```
|
||||
@@ -233,7 +235,7 @@ go test ./app/ -v -run "TestApi/app/(user|order)"
|
||||
go test ./app/ -v -run "TestApi/.*/.*未登录"
|
||||
```
|
||||
|
||||
> **注意**:`TestMain` 中的 `os.Chdir("..")` 确保 `go test ./app/` 的工作目录为项目根目录,使配置文件路径和模板输出路径正确。
|
||||
> **注意**:`WorkDir("..")` 在 Init 前切换工作目录,确保 `go test ./app/` 时配置与模板输出路径正确。
|
||||
>
|
||||
> 全量 `go test ./app/... -v`(不带 `-run`)会跑完所有用例并触发覆盖率扫描,项目变大后很慢且容易牵连无关失败。**仅在 CI / 发版前使用**,说明见文末。
|
||||
|
||||
@@ -511,25 +513,82 @@ type ApiTestDef struct {
|
||||
### TestApp
|
||||
|
||||
```go
|
||||
// NewTestApp 创建测试应用
|
||||
// configPath: 配置文件路径
|
||||
// projects: TestProj 注册所有项目的路由和测试
|
||||
// listeners: 可选,与 SetConnectListener 相同的请求拦截器
|
||||
func NewTestApp(configPath string, projects TestProj, listeners ...func(*Context) bool) *TestApp
|
||||
// Test 链式入口(延迟 Init,便于先 WorkDir)
|
||||
func Test(configPath string, listeners ...func(*Context) bool) *TestApp
|
||||
|
||||
func (app *TestApp) WorkDir(dir string) *TestApp
|
||||
func (app *TestApp) Swagger(args ...string) *TestApp // title / version / outputDir 均可选
|
||||
func (app *TestApp) Proj(projects TestProj) *TestApp
|
||||
func (app *TestApp) Flows(flows FlowTest) *TestApp // 可选:多接口业务流程
|
||||
func (app *TestApp) Run(m *testing.M) int // 信号停测 + m.Run + 覆盖率 + Swagger 收尾
|
||||
|
||||
// RunTests 运行所有注册的测试(每个方法级别自动事务隔离)
|
||||
func (app *TestApp) RunTests(t *testing.T)
|
||||
|
||||
// PrintCoverage 输出覆盖率报告,返回报告结构体
|
||||
func (app *TestApp) PrintCoverage() CoverageReport
|
||||
|
||||
// GenerateSwagger 生成 API 调试控制台(api-spec.json + index.html)
|
||||
func (app *TestApp) GenerateSwagger(title, version, outputDir string) error
|
||||
|
||||
// DB 获取测试应用的数据库实例(在事务内)
|
||||
func (app *TestApp) DB() *HoTimeDB
|
||||
```
|
||||
|
||||
### 业务流程联调(Flows)
|
||||
|
||||
与单接口 `CtrTest` 并列,用于多接口串起来的业务跑通。整条 Flow 共用一次测试事务,结束回滚。
|
||||
|
||||
**FlowDef 字段**
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `Group` | 控制台侧栏分组(如 `order`) |
|
||||
| `Desc` | 流程短标题 |
|
||||
| `Prep` | 业务说明 / 前置与数据准备(控制台「业务说明」) |
|
||||
| `Func` | `func(f *Flow)` |
|
||||
|
||||
**Flow API**
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `f.Step(name, path)` | 新步骤,返回 `*Api`(记录归入 flows) |
|
||||
| `f.AtPath(path)` | 同流程内换 path,不增步骤序号 |
|
||||
| `f.WithSession(s)` | 后续步骤携带 session |
|
||||
| `f.DB()` / `f.Resp()` | 流程事务库 / 最近一步响应 |
|
||||
|
||||
```go
|
||||
Flows(FlowTest{
|
||||
"checkout": {
|
||||
Group: "order",
|
||||
Desc: "下单支付跑通",
|
||||
Prep: "先创建订单再支付;步骤可用 FromCase 绑定单接口验收用例。",
|
||||
Func: func(f *Flow) {
|
||||
f.Step("创建订单", "/app/order/create").
|
||||
FromCase("正常创建").
|
||||
Note("创建后取 order id 供支付步骤使用").
|
||||
WithSession(Map{"admin_id": 1}).
|
||||
Post("创建", 0, Map{"id": int64(1)})
|
||||
orderID := f.Resp().GetBody().GetMap("result").GetCeilInt64("id")
|
||||
f.Step("支付", "/app/order/pay").
|
||||
WithSession(Map{"admin_id": 1}).
|
||||
JSON(Map{"order_id": orderID}).
|
||||
Post("支付成功", 0)
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**FromCase**
|
||||
|
||||
- 按 path + 用例名加载请求模板:优先本轮已跑的单接口记录,否则读已有 `api-spec.json`
|
||||
- 步骤跑完后回写该单接口 case 的 response / passed
|
||||
- 无模板时当前为**静默不填**(见 [已知局限](#已知局限与后续));建议全量或先跑绑定接口再跑 flow
|
||||
- 示例:`example/app/flow_test.go`
|
||||
|
||||
控制台:侧栏「业务流程 → group → flow 名」;步骤左栏含关联验收用例 / 备注 / 请求,右栏响应;点 path 弹窗看接口只读详情。
|
||||
`-run`:`go test ./app/ -run "TestApi/flows/checkout" -v`
|
||||
|
||||
### 优雅停测
|
||||
|
||||
`Run(m)` 内监听 SIGINT/SIGTERM;测试代码可调 `testApp.RequestStop()` 模拟。
|
||||
|
||||
行为:停收后续接口/流程 → 当前子测跑完 → 回滚测试事务 → 已 flush 的 swagger **保留**(不全量 prune)。
|
||||
示例:`example/app/app_test.go` 的 `TestInterruptStop`。
|
||||
|
||||
---
|
||||
|
||||
## 链式 API 参考
|
||||
@@ -546,7 +605,9 @@ func (app *TestApp) DB() *HoTimeDB
|
||||
| `a.File(field, name, content)` | 设置上传文件 | `*ApiCase` |
|
||||
| `a.Note(note)` | 为下一条用例设置备注,备注显示在调试控制台 | `*ApiCase` |
|
||||
| `a.Verify(fn)` | 设置请求后的校验函数(等同于先创建 ApiCase 再 Verify,适合纯 GET 无数据场景) | `*ApiCase` |
|
||||
| `a.FromCase(name)` | 绑定单接口验收用例:加载其请求模板;跑通后回写 response/passed | `*ApiCase` |
|
||||
| `a.WithSession(s)` | 返回携带 session 的新 Api | `*Api` |
|
||||
| `a.RunSub(desc, fn)` | 接口 Func 内再分子场景,便于 `-run` 收窄 | — |
|
||||
| `a.Get(desc, status, expect...)` | 直接 GET 请求(无数据场景) | `*ApiResponse` |
|
||||
| `a.Post(desc, status, expect...)` | 直接 POST 请求(无数据场景) | `*ApiResponse` |
|
||||
| `a.Put(desc, status, expect...)` | 直接 PUT 请求 | `*ApiResponse` |
|
||||
@@ -566,6 +627,7 @@ func (app *TestApp) DB() *HoTimeDB
|
||||
| `c.File(field, name, content)` | 设置上传文件 | `*ApiCase` |
|
||||
| `c.Note(note)` | 为本条用例添加可选备注,备注显示在调试控制台用例详情中 | `*ApiCase` |
|
||||
| `c.WithSession(s)` | 覆盖本次请求的 session | `*ApiCase` |
|
||||
| `c.FromCase(name)` | 绑定单接口验收用例名并加载请求模板(若尚未加载) | `*ApiCase` |
|
||||
| `c.Verify(fn)` | 设置请求后的统一校验函数,可用 `a.Resp()` 断言响应值 + `a.DB()` 校验数据库(详见 [Verify 统一校验](#4-verify-统一校验)) | `*ApiCase` |
|
||||
| `c.Get(desc, status, expect...)` | 终端:发送 GET 请求并断言 | `*ApiResponse` |
|
||||
| `c.Post(desc, status, expect...)` | 终端:发送 POST 请求并断言 | `*ApiResponse` |
|
||||
@@ -720,39 +782,33 @@ that.Db.Action(func(db HoTimeDB) bool {
|
||||
|
||||
## API 调试控制台
|
||||
|
||||
`GenerateSwagger` 按模块生成独立的交互式 API 调试控制台(暗色高对比度主题),每个模块一个子目录,支持独立分发。
|
||||
通过链式 `.Swagger(title, version)` 启用。
|
||||
|
||||
```go
|
||||
testApp.GenerateSwagger(
|
||||
"My API", // 文档标题
|
||||
"2.1.0", // 版本号
|
||||
testApp.Config.GetString("tpt"), // 输出目录(如 "tpt")
|
||||
)
|
||||
```
|
||||
**增量写入**:每个接口 / 流程跑完即 flush;本轮无记录则不覆盖成「未测」;未全量跑完不 prune 已删接口;仅全量完成才清理孤儿模块。Ctrl+C / SIGTERM 走优雅停测后再收尾。
|
||||
|
||||
输出目录默认 `config` 中的 `tpt`。
|
||||
|
||||
### 生成目录结构
|
||||
|
||||
每个模块(`TestProj` 中的项目名)生成独立子目录,同时自动生成根导航页:
|
||||
|
||||
```
|
||||
{outputDir}/swagger/
|
||||
├── index.html ← 根导航页(API 文档中心,卡片式链接到各模块)
|
||||
├── api/
|
||||
│ ├── api-spec.json ← api 模块的 API 规范(路由、测试用例、请求/响应数据)
|
||||
│ └── index.html ← api 模块的调试控制台
|
||||
├── admin/
|
||||
│ ├── api-spec.json
|
||||
├── index.html ← 根导航页
|
||||
├── app/
|
||||
│ ├── api-spec.json ← endpoints + flows
|
||||
│ └── index.html
|
||||
└── portal/
|
||||
├── api-spec.json
|
||||
└── index.html
|
||||
└── ...
|
||||
```
|
||||
|
||||
访问地址:`http://localhost:8081/swagger/`(根导航页),`http://localhost:8081/swagger/api/`(api 模块)
|
||||
访问:`http://localhost:8081/swagger/`(根导航),`http://localhost:8081/swagger/app/`(模块)。
|
||||
|
||||
> **独立分发**:每个模块目录可以单独拷贝和部署,不依赖其他模块。根导航页自动扫描子目录生成链接。
|
||||
### 控制台能力
|
||||
|
||||
控制台支持三级导航、关键字搜索、用例预填、全局认证(Header/URL/Cookie)、cURL 生成、覆盖率可视化等功能。
|
||||
| 区域 | 行为 |
|
||||
|------|------|
|
||||
| 侧栏 | 业务流程 → group → flow 名;接口仍按 project/ctr 三级 |
|
||||
| 流程页 | Prep 业务说明;步骤默认展开;左备注/关联验收用例/请求(cURL),右响应;点 path 弹窗只读详情(Esc 关闭) |
|
||||
| 单接口 | 用例预填、全局认证;**发送结果 cURL 随左侧 Header/Query/JSON/Form 实时同步** |
|
||||
| 发送 | 浏览器 `fetch` 打当前源站,**需已启动服务**(与 httptest 零启动测试不同) |
|
||||
|
||||
---
|
||||
|
||||
@@ -761,7 +817,7 @@ testApp.GenerateSwagger(
|
||||
框架自动处理以下问题,编写测试时无需关心:
|
||||
|
||||
- **并发保护**:`testTx` 是单连接,框架通过 `testMu` 互斥锁自动序列化所有数据库操作,业务代码中的 `go func()` 协程不会导致 `busy buffer` 错误
|
||||
- **缓存隔离**:`NewTestApp` 启动时自动禁用 DB/Redis 缓存,避免与测试事务产生锁冲突;Session 通过 `WithSession()` 直接注入 Memory 缓存
|
||||
- **缓存隔离**:启动时自动禁用 DB/Redis 缓存,避免与测试事务产生锁冲突;Session 通过 `WithSession()` 直接注入 Memory 缓存
|
||||
|
||||
### 一次性数据的处理
|
||||
|
||||
@@ -865,21 +921,21 @@ a.Verify(func(a *Api) error {
|
||||
go test ./app/... -v
|
||||
```
|
||||
|
||||
`TestMain` 里常见的收尾逻辑(全量跑完后才有意义):
|
||||
`TestMain` 推荐写法(`Run` 已含覆盖率与 Swagger 收尾):
|
||||
|
||||
```go
|
||||
func TestMain(m *testing.M) {
|
||||
// ...
|
||||
code := m.Run()
|
||||
testApp.PrintCoverage() // 全量跑完后输出覆盖率
|
||||
testApp.GenerateSwagger("My API", "1.0.0", testApp.Config.GetString("tpt"))
|
||||
os.Exit(code)
|
||||
testApp = Test("config/config.json").
|
||||
WorkDir("..").
|
||||
Swagger("My API", "1.0.0").
|
||||
Proj(TestProj{"app": {Proj: Project, Tests: ProjectTest}})
|
||||
os.Exit(testApp.Run(m))
|
||||
}
|
||||
```
|
||||
|
||||
### 覆盖率报告
|
||||
|
||||
`PrintCoverage()` 自动对比 `Proj`(所有路由)和 `ProjTest`(有测试的路由):
|
||||
`PrintCoverage()` 自动对比 `Proj`(所有路由)和 `ProjTest`(有测试的路由)。**Flow 步骤不计入覆盖率**(`FlowName != ""` 的记录会被跳过)。
|
||||
|
||||
**输出示例(全部通过):**
|
||||
|
||||
@@ -937,3 +993,20 @@ func TestMain(m *testing.M) {
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 已知局限与后续
|
||||
|
||||
| 优先级 | 缺口 | 说明 |
|
||||
|--------|------|------|
|
||||
| 高 | 值级断言助手 | 结构校验有,值断言靠 Verify 手写 |
|
||||
| 高 | DB 断言助手 | 无 AssertCount / AssertRow 一类 |
|
||||
| 高 | FromCase 无模板显式失败 | 现静默不填,易空 body 误测 |
|
||||
| 中 | 自定义 Header / Cookie API | 控制台有,测试链无 `WithHeader` |
|
||||
| 中 | HTTP 状态码 / 响应头断言 | StatusCode 有写入,无公开断言 |
|
||||
| 中 | 覆盖率含 Flow | PrintCoverage 跳过 Flow 记录 |
|
||||
| 低 | PATCH、多文件上传 | 当前边界 |
|
||||
| 低 | 框架级 Seed/Fixture | 依赖业务自写 `a.DB()` 准备 |
|
||||
|
||||
规划表见 [ROADMAP_改进规划.md](ROADMAP_改进规划.md)。
|
||||
|
||||
Reference in New Issue
Block a user