Files
hotime/.cursor/plans/api测试框架完整实现_ce7fcbde.plan.md
T
hoteas de17ecbfd5 chore(logging): 更新日志重定向与捕获功能
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交
- 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获
- 更新 README 文档,增加对日志重定向功能的说明
2026-07-13 07:45:51 +08:00

292 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: API测试框架完整实现
overview: 在 hotimev1.5 框架中实现完整的 API 测试基础设施:链式测试 API、事务回滚隔离、覆盖率追踪、Swagger 文档自动生成。
todos:
- id: db-testtx
content: db/db.go:加 testTx 字段 + BeginTestTx/RollbackTestTx 方法
status: completed
- id: db-query
content: db/query.goQuery(行64) 和 Exec(行120) 加 testTx 优先判断
status: completed
- id: db-transaction
content: db/transaction.goAction 加 testTx 传递 + SAVEPOINT 分支
status: completed
- id: testing-helper
content: 新增 testing_helper.goTestApp、NewTestApp、SetupForTest、RunTests(事务隔离)、PrintCoverage
status: completed
- id: testing-api
content: 新增 testing_api.go:注册类型 + Api/ApiCase/ApiResponse 链式 API + TestCollector
status: completed
- id: testing-swagger
content: 新增 testing_swagger.goGenerateSwagger 合并所有项目生成 Swagger
status: completed
- id: xbc-integration
content: xbc 业务层:user.go 加 UserTest、init.go 加 ProjectTest、新增 app_test.go
status: completed
isProject: false
---
# API 测试框架完整实现
## 一、事务回滚隔离(db 层改造)
核心思路:给 `HoTimeDB` 增加 `testTx` 字段,优先级 `testTx > Tx > DB`。测试模式下 `Action()` 用 MySQL `SAVEPOINT` 代替真事务,保证嵌套事务都在外层测试事务内。
### 1. [db/db.go](d:/work/hotimev1.5/db/db.go) -- 加字段 + 方法
```go
// HoTimeDB 结构体新增字段
testTx *sql.Tx // 测试事务:设置后所有操作都在此事务内
// 新增两个方法
func (that *HoTimeDB) BeginTestTx() error
func (that *HoTimeDB) RollbackTestTx() error
```
### 2. [db/query.go](d:/work/hotimev1.5/db/query.go) -- Query(行64) 和 Exec(行120) 各加一个判断
```go
// 原:if that.Tx != nil { ... } else { ... }
// 改:
if that.testTx != nil {
resl, err = that.testTx.Query(query, processedArgs...)
} else if that.Tx != nil {
resl, err = that.Tx.Query(query, processedArgs...)
} else {
resl, err = db.Query(query, processedArgs...)
}
// Exec 同理
```
### 3. [db/transaction.go](d:/work/hotimev1.5/db/transaction.go) -- Action 加 SAVEPOINT 分支
```go
func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSuccess bool) {
db := HoTimeDB{
// ...现有字段拷贝...
testTx: that.testTx, // 新增:传递 testTx
}
// 新增:测试模式用 SAVEPOINT
if that.testTx != nil {
spName := "sp_" + Md5(ObjToStr(RandX(100000, 999999)))[:8]
_, _ = that.testTx.Exec("SAVEPOINT " + spName)
db.Tx = that.testTx
isSuccess = action(db)
if !isSuccess {
_, _ = that.testTx.Exec("ROLLBACK TO SAVEPOINT " + spName)
} else {
_, _ = that.testTx.Exec("RELEASE SAVEPOINT " + spName)
}
return isSuccess
}
// 以下原有逻辑不变...
}
```
### 3.1 testTx 并发保护(调试发现的关键问题)
**问题现象:** 测试运行时出现 `[mysql] busy buffer``bad connection` 错误,导致测试挂起。
**根因分析:** `testTx` 是单个 `*sql.Tx` 连接,MySQL 驱动不允许在同一连接上并发执行查询。业务代码中存在 `go func()` 启动的 goroutine(如 `syncGoodsToYst``stats.go` 中的统计协程),这些 goroutine 会并发访问 `testTx`,导致驱动层的 `busy buffer` 错误。
**解决方案:**`HoTimeDB` 中新增 `testMu *sync.Mutex`,在 `BeginTestTx()` 时初始化。所有通过 `testTx` 执行的 Query、Exec、SAVEPOINT 操作都通过 `testMu` 加锁保护,确保串行化访问。
```go
// db/db.go - HoTimeDB 新增字段
testMu *sync.Mutex // 保护 testTx 单连接不被并发访问
// db/db.go - BeginTestTx 中初始化
that.testMu = &sync.Mutex{}
// db/query.go - Query 中加锁
if that.testTx != nil {
if that.testMu != nil { that.testMu.Lock() }
resl, err = that.testTx.Query(query, processedArgs...)
// ... 消费 result set ...
if that.testMu != nil { that.testMu.Unlock() }
}
// db/query.go - Exec 中加锁
if that.testTx != nil {
if that.testMu != nil { that.testMu.Lock() }
resl, e = that.testTx.Exec(query, processedArgs...)
if that.testMu != nil { that.testMu.Unlock() }
}
// db/transaction.go - SAVEPOINT 操作加锁
if that.testMu != nil { that.testMu.Lock() }
_, _ = that.testTx.Exec("SAVEPOINT " + spName)
if that.testMu != nil { that.testMu.Unlock() }
```
**注意事项:**
- 生产环境不受影响(`testMu` 默认 nil,所有锁检查都有 nil 保护)
- 互斥锁确保 goroutine 中的 DB 操作排队执行而非并发冲突
- Query 锁的范围包括结果集消费(`that.Row(resl)`),防止未读完数据就发起新查询
**改动量:** 3 个文件,约 50 行核心改动 + mutex 保护约 30 行,生产代码零影响(testTx/testMu 默认 nil)。
### 3.2 测试模式下禁用 DB/Redis 缓存(调试发现的关键问题)
**问题现象:** 测试运行时出现 `Error 1205: Lock wait timeout exceeded` 错误,指向 `DELETE FROM cached` 操作。
**根因分析:** 框架的 `HoTimeCache` 支持三级缓存(memory > redis > db)。在测试模式下,DB 缓存的读写操作(如 `DELETE FROM cached`)会尝试使用独立连接,而 `testTx` 持有事务锁,导致 `Lock wait timeout`。即使走 `testTx`,缓存的并发读写也与事务隔离产生冲突。
**解决方案:**
1.`cache/cache.go` 中新增 `DisableDbCache()` 方法:
```go
func (that *HoTimeCache) DisableDbCache() {
that.dbCache = nil
that.redisCache = nil
}
```
2.`testing_helper.go``NewTestApp` 中调用:
```go
if app.HoTimeCache != nil {
app.HoTimeCache.DisableDbCache()
}
```
3.`db/crud.go``Select` 方法中,当 `testTx` 激活时跳过缓存逻辑:
```go
if that.testTx == nil {
// 原有缓存读写逻辑
}
```
**影响范围:** 测试期间仅使用 memory 缓存,不影响生产环境。Session 在测试中通过 `WithSession()` 直接注入,不依赖 DB/Redis 缓存。
### 3.3 测试模式下 Query 的重试策略调整
**问题:** `queryWithRetry``execWithRetry` 在遇到连接错误时会调用 `db.Ping()` 尝试重连,但 `testTx` 是单连接事务,Ping 会创建新连接导致 `busy buffer`
**解决:**`testTx != nil` 时,跳过 `db.Ping()` 和重试逻辑,直接返回错误。
## 二、链式测试 API
### 4. 新增 [testing_helper.go](d:/work/hotimev1.5/testing_helper.go)
- `TestApp` 结构体(包含 `*Application``projs TestProj``collector *TestCollector`
- `NewTestApp(configPath, TestProj, listeners...)` -- 初始化 Application、注册路由,不启动 HTTP 服务
- `SetupForTest(router Router)` -- 复用 `Run()` 的路由初始化逻辑
- `TestRequest(method, path, body) TestResponse` / `TestRequestWithSession(...)` -- 通过 `httptest` 发请求
- `TestResponse` 结构体
- `RunTests(t)` -- 遍历 TestProj,每个方法级别 `BeginTestTx() + defer RollbackTestTx()`
### 5. 新增 [testing_api.go](d:/work/hotimev1.5/testing_api.go)
注册类型:
- `TestProj map[string]TestProjDef`
- `TestProjDef struct { Proj Proj; Tests ProjTest }`
- `ProjTest map[string]CtrTest`
- `CtrTest map[string]ApiTestDef`
- `ApiTestDef struct { Desc string; Func func(a *Api) }`
链式 API
- `Api` -- 起点,提供 `JSON/Query/Form/File/WithSession` 数据设置和 `Get/Post/Put/Delete` 直接终端
- `ApiCase` -- 链式构建器,支持叠加多种数据类型,终端操作 `Get/Post/Put/Delete(desc, status, msg...)`
- `ApiResponse` -- 响应对象,提供 `GetStatus/GetResult/GetMsg/GetBody/Fail`
- 每个终端方法内自动记录 `TestRecord``TestCollector`
### 6. 新增 [testing_swagger.go](d:/work/hotimev1.5/testing_swagger.go)
- `GenerateSwagger(title, version, outputDir)` -- 遍历 TestProj 生成合并的 OpenAPI 3.0 JSON + Swagger UI HTML
- 利用覆盖率数据,未覆盖接口在文档中标注"暂无测试用例"
## 三、覆盖率追踪
集成在 `testing_helper.go` 中:
- `CoverageReport` / `MethodCoverage` / `TestRecord` / `TestCollector` 类型
- `PrintCoverage()` -- 对比 `Proj`(所有路由)和 `ProjTest`(有测试的路由),输出覆盖率报告
- 运行时追踪:每个 `Post/Get/Put/Delete` 终端方法自动收集 `TestRecord`(路径、用例名、通过/失败、耗时)
- 最终输出:总接口数、已覆盖数、覆盖率百分比、每个接口的用例数和通过/失败计数
## 四、业务层接入(xbc 示例)
### 7. 修改 [app/app/user.go](d:/work/your-app/app/user.go) -- 末尾添加 `var UserTest = CtrTest{...}`
```go
var UserTest = CtrTest{
"login": {"用户登录", func(a *Api) {
a.JSON(Map{"name": "138", "password": "123456"}).Post("正常登录", 0)
// ...
}},
"create": {"用户注册", func(a *Api) {
phone := "138" + ObjToStr(RandX(10000000, 99999999))
// 不需要 defer 清理,事务自动回滚
a.JSON(Map{"phone": phone, "password": "123456"}).Post("正常注册", 0)
a.JSON(Map{"phone": phone, "password": "123456"}).Post("重复注册", 3, "该手机号已被注册")
}},
}
```
### 8. 修改 [app/app/init.go](d:/work/your-app/app/init.go) -- 添加 `var ProjectTest = ProjTest{...}`
### 9. 新增 [app/app/app_test.go](d:/work/your-app/app/app_test.go)
```go
func TestMain(m *testing.M) {
testApp = NewTestApp("../config/config.json", TestProj{
"app": {Project, ProjectTest},
})
code := m.Run()
testApp.PrintCoverage()
testApp.GenerateSwagger("HoTime API", "2.1.0", testApp.Config.GetString("tpt"))
os.Exit(code)
}
func TestApi(t *testing.T) {
testApp.RunTests(t)
}
```
## 五、文件改动汇总
### hotimev1.5 框架层
- **修改** `db/db.go` -- 加 testTx + testMu 字段,BeginTestTx(初始化 mutex/RollbackTestTx
- **修改** `db/query.go` -- Query/Exec 各加 testTx 判断 + testMu 互斥锁保护 + 跳过重试逻辑
- **修改** `db/transaction.go` -- Action 加 SAVEPOINT 分支 + testTx/testMu 传递 + SAVEPOINT 操作加锁
- **修改** `db/crud.go` -- Select 中 testTx 激活时跳过缓存逻辑
- **修改** `cache/cache.go` -- 新增 DisableDbCache() 方法 + 新增 SessionsGet/SessionsSet/SessionsDelete 批量操作
- **修改** `session.go` -- 对接批量 Session 缓存操作
- **修改** `testing_helper.go` -- NewTestApp 中调用 DisableDbCache()
- **新增** `testing_helper.go` -- TestApp、RunTests、覆盖率
- **新增** `testing_api.go` -- 链式 API + TestCollector
- **新增** `testing_swagger.go` -- Swagger 生成
### xbc 业务层
- **修改** `app/user.go` -- 末尾添加 UserTest
- **修改** `app/init.go` -- 添加 ProjectTest
- **新增** `app/app_test.go` -- 测试入口(含 Swagger 生成)
- **新增** `app/*_test.go` -- 30+ 个控制器测试文件
- **新增** `wx/wx_test.go` + `wx/*_test.go` -- 微信端测试入口 + 7 个控制器测试
- **新增** `dd/dd_test.go` + `dd/*_test.go` -- 钉钉端测试入口 + 6 个控制器测试
- **新增** `customer/customer_h5_test.go` + `customer/*_test.go` -- H5 客户端测试
- **新增** `sms/sms_test.go` + `sms/sms_ctr_test.go` -- 短信服务测试
- **修改** `app/wechat.go` -- 修复 log.Println -> log.Printfgo vet 警告)
- **修改** `customer/init.go` -- 修复 log.Println -> log.Printfgo vet 警告)
## 六、已知问题与调试经验
### 问题一:busy buffer / bad connection
- **现象:** 测试运行数秒后出现 `[mysql] busy buffer`,随后 `bad connection`,测试挂起
- **根因:** 业务代码中 `go func()` 启动的 goroutine(如 `app/goods.go:syncGoodsToYst``app/stats.go` 中的统计协程)并发访问 `testTx` 单连接
- **解决:** 在 `HoTimeDB` 中新增 `testMu *sync.Mutex`,所有 testTx 的 Query/Exec/SAVEPOINT 操作加互斥锁
- **验证方式:** 通过在 query.go 中插入带时间戳和 goroutine 计数的日志,观察到操作严格串行化,无重叠
### 问题二:Lock wait timeout
- **现象:** `Error 1205: Lock wait timeout exceeded`,指向 `DELETE FROM cached`
- **根因:** `HoTimeCache` 的 DB 缓存通过独立连接操作 `cached` 表,与 `testTx` 事务锁冲突
- **解决:** 测试启动时调用 `DisableDbCache()` 禁用 DB/Redis 缓存,仅保留 memory 缓存
### 问题三:go vet 编译失败
- **现象:** `go test``go vet` 警告 `log.Println call has possible Printf formatting directive %v` 而拒绝运行
- **根因:** `app/wechat.go``customer/init.go` 中误用 `log.Println` 传入 `%v` 格式化指令
- **解决:** 改为 `log.Printf`