因为直接调用http.ServeHTTP会绕过Gin的中间件链、路由匹配和上下文初始化,导致c.Param()返回空、c.ShouldBindJSON()可能panic;官方推荐用gin.CreateTestContext+httptest.NewRequest复现完整请求生命周期。

为什么不用 http.ServeHTTP 而要用 gin.CreateTestContext
直接调用 router.ServeHTTP(w, req) 会绕过 Gin 的核心执行链——中间件不执行、路由不匹配、c.Param() 返回空、c.ShouldBindJSON() 可能 panic。这不是“测得慢”,而是“根本没走真实流程”。
官方推荐且唯一能复现真实行为的方式是:gin.CreateTestContext + httptest.NewRequest。它生成的 *gin.Context 包含完整生命周期,包括中间件链触发、参数解析、上下文初始化。
- 必须手动赋值:
c.Request = req,否则上下文和请求脱钩 - JSON 请求务必设 header:
req.Header.Set("Content-Type", "application/json"),否则c.ShouldBindJSON静默失败 - 路径参数(如
/users/:id)需显式设置:c.Params = gin.Params{{Key: "id", Value: "123"}},Gin 不会自动从 URL 解析
DAO 层 mock 用 sqlmock,service 层 mock 用 gomock
两者职责不同,不是二选一,而是分层协作:DAO 层操作 *sql.DB,service 层依赖 repository 接口。混用会导致断言失焦或 mock 失效。
-
sqlmock拦截真实 DB 调用,验证 SQL 语句、参数、执行次数,适合测试 DAO 函数是否发出了预期查询 -
gomock基于 interface 生成 mock 实现,用于 service 层隔离外部依赖,比如让userRepo.GetUserByID返回固定 error 或数据 - 关键限制:
gomock要求被 mock 的接口定义在独立包(如repo.UserRepo),不能定义在 test 文件或 service 包内,否则mockgen找不到 -
sqlmock默认严格模式,未声明的查询直接 panic;调试时可临时用sqlmock.New(sqlmock.QueryMatcherEqual)改为宽松匹配
c.BindJSON 和 c.ShouldBindJSON 在测试中行为完全不同
这是 Gin 测试里最常踩的坑:用 c.BindJSON 会让 handler 自动写 400 响应并 return,你却还在检查 w.Body.String(),结果误判为“成功返回空数据”。
-
c.BindJSON:失败即终止 handler,写响应、设状态码,不返回 error —— 测试时必须断言w.Code == http.StatusBadRequest -
c.ShouldBindJSON:只校验、不响应,返回 error,把控制权交还给你 —— 单元测试首选,便于主动构造错误场景(如传数字给 string 字段)并验证处理逻辑 - 别依赖
gin.Default():它自带Logger()和Recovery(),前者可能往w.Body写日志干扰断言,后者会吞掉 panic 导致 500 响应掩盖真实问题
中间件怎么测才不算白测
中间件本质是 func(*gin.Context),但单独调用它和集成进 router 是两回事。测错一层,就等于没测。
- 单元测中间件:传入 mock context(
gin.CreateTestContext构造),检查它是否调用c.Abort()、c.Set()或修改c.Writer.Status(),不关心下游 handler - 集成测中间件:把中间件注册进
gin.Engine,走完整请求流程,验证它是否影响最终响应(比如鉴权中间件是否让未登录请求返回 401) - 测试用的 router 必须干净:你在 main.go 里写的
r.Use(authMw)对测试文件里的新gin.New()完全无效 —— 必须显式注册,或把 router 初始化抽成函数,如NewRouter(mws ...gin.HandlerFunc)
复杂点在于中间件之间有状态传递(c.Set()/c.Get())、执行顺序依赖、Abort 后续链中断,这些都得在对应层级覆盖到,漏掉任意一层,mock 就只是个摆设。


















