- 在 HoTimeDB 中新增 testMu 互斥锁,确保 testTx 的串行访问,解决并发冲突问题 - 在 BeginTestTx 中初始化 testMu,确保测试事务的安全性 - 在 HoTimeCache 中新增 DisableDbCache 方法,测试模式下禁用 DB 和 Redis 缓存,避免锁等待超时 - 更新 Select 方法,确保在 testTx 激活时跳过缓存逻辑,提升测试稳定性 - 优化 Swagger 生成逻辑,支持模块化输出和导航页生成 - 移除冗余的调试日志代码,提升代码整洁性
12 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| API测试框架完整实现 | 在 hotimev1.5 框架中实现完整的 API 测试基础设施:链式测试 API、事务回滚隔离、覆盖率追踪、Swagger 文档自动生成。 |
|
false |
API 测试框架完整实现
一、事务回滚隔离(db 层改造)
核心思路:给 HoTimeDB 增加 testTx 字段,优先级 testTx > Tx > DB。测试模式下 Action() 用 MySQL SAVEPOINT 代替真事务,保证嵌套事务都在外层测试事务内。
1. db/db.go -- 加字段 + 方法
// HoTimeDB 结构体新增字段
testTx *sql.Tx // 测试事务:设置后所有操作都在此事务内
// 新增两个方法
func (that *HoTimeDB) BeginTestTx() error
func (that *HoTimeDB) RollbackTestTx() error
2. db/query.go -- Query(行64) 和 Exec(行120) 各加一个判断
// 原: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 -- Action 加 SAVEPOINT 分支
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 加锁保护,确保串行化访问。
// 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,缓存的并发读写也与事务隔离产生冲突。
解决方案:
- 在
cache/cache.go中新增DisableDbCache()方法:
func (that *HoTimeCache) DisableDbCache() {
that.dbCache = nil
that.redisCache = nil
}
- 在
testing_helper.go的NewTestApp中调用:
if app.HoTimeCache != nil {
app.HoTimeCache.DisableDbCache()
}
- 在
db/crud.go的Select方法中,当testTx激活时跳过缓存逻辑:
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
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
注册类型:
TestProj map[string]TestProjDefTestProjDef struct { Proj Proj; Tests ProjTest }ProjTest map[string]CtrTestCtrTest map[string]ApiTestDefApiTestDef 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
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. 修改 xbc/app/user.go -- 末尾添加 var UserTest = CtrTest{...}
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. 修改 xbc/app/init.go -- 添加 var ProjectTest = ProjTest{...}
9. 新增 xbc/app/app_test.go
func TestMain(m *testing.M) {
testApp = NewTestApp("../config/config.json", TestProj{
"app": {Project, ProjectTest},
})
code := m.Run()
testApp.PrintCoverage()
testApp.GenerateSwagger("小帮菜 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.Printf(go vet 警告) - 修改
customer/init.go-- 修复 log.Println -> log.Printf(go 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