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

12 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
API测试框架完整实现 在 hotimev1.5 框架中实现完整的 API 测试基础设施:链式测试 API、事务回滚隔离、覆盖率追踪、Swagger 文档自动生成。
id content status
db-testtx db/db.go:加 testTx 字段 + BeginTestTx/RollbackTestTx 方法 completed
id content status
db-query db/query.goQuery(行64) 和 Exec(行120) 加 testTx 优先判断 completed
id content status
db-transaction db/transaction.goAction 加 testTx 传递 + SAVEPOINT 分支 completed
id content status
testing-helper 新增 testing_helper.goTestApp、NewTestApp、SetupForTest、RunTests(事务隔离)、PrintCoverage completed
id content status
testing-api 新增 testing_api.go:注册类型 + Api/ApiCase/ApiResponse 链式 API + TestCollector completed
id content status
testing-swagger 新增 testing_swagger.goGenerateSwagger 合并所有项目生成 Swagger completed
id content status
xbc-integration xbc 业务层:user.go 加 UserTest、init.go 加 ProjectTest、新增 app_test.go completed
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 bufferbad connection 错误,导致测试挂起。

根因分析: testTx 是单个 *sql.Tx 连接,MySQL 驱动不允许在同一连接上并发执行查询。业务代码中存在 go func() 启动的 goroutine(如 syncGoodsToYststats.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,缓存的并发读写也与事务隔离产生冲突。

解决方案:

  1. cache/cache.go 中新增 DisableDbCache() 方法:
func (that *HoTimeCache) DisableDbCache() {
    that.dbCache = nil
    that.redisCache = nil
}
  1. testing_helper.goNewTestApp 中调用:
if app.HoTimeCache != nil {
    app.HoTimeCache.DisableDbCache()
}
  1. db/crud.goSelect 方法中,当 testTx 激活时跳过缓存逻辑:
if that.testTx == nil {
    // 原有缓存读写逻辑
}

影响范围: 测试期间仅使用 memory 缓存,不影响生产环境。Session 在测试中通过 WithSession() 直接注入,不依赖 DB/Redis 缓存。

3.3 测试模式下 Query 的重试策略调整

问题: queryWithRetryexecWithRetry 在遇到连接错误时会调用 db.Ping() 尝试重连,但 testTx 是单连接事务,Ping 会创建新连接导致 busy buffer

解决:testTx != nil 时,跳过 db.Ping() 和重试逻辑,直接返回错误。

二、链式测试 API

4. 新增 testing_helper.go

  • TestApp 结构体(包含 *Applicationprojs TestProjcollector *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]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
  • 每个终端方法内自动记录 TestRecordTestCollector

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. 修改 app/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. 修改 app/app/init.go -- 添加 var ProjectTest = ProjTest{...}

9. 新增 app/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("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:syncGoodsToYstapp/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 testgo vet 警告 log.Println call has possible Printf formatting directive %v 而拒绝运行
  • 根因: app/wechat.gocustomer/init.go 中误用 log.Println 传入 %v 格式化指令
  • 解决: 改为 log.Printf