Handler测试必须用gin.CreateTestContext,因其能复现真实请求生命周期;需手动设c.Request、c.Params、Header,并启用gin.TestMode;依赖须通过接口注入并校验nil;手写fake比gomock更可靠;main函数是唯一合法依赖组装点。

Handler 测试必须用 gin.CreateTestContext,不是 http.ServeHTTP
直接调用 router.ServeHTTP 会跳过 Gin 的上下文初始化、中间件链和路由匹配逻辑,导致 c.Param 返回空、c.ShouldBindJSON 静默失败、c.Get("user_id") panic。真实请求生命周期只能靠 gin.CreateTestContext 复现。
实操要点:
- 先调
gin.CreateTestContext创建*gin.Context,再手动赋值c.Request = req - 路径参数必须显式设置:
c.Params = gin.Params{{Key: "id", Value: "123"}},否则c.Param("id")拿不到值 - Header 要手动设:
req.Header.Set("Content-Type", "application/json"),不然 JSON 绑定直接跳过 - 测试前务必调
gin.SetMode(gin.TestMode),否则日志刷屏、重定向中间件干扰断言
依赖必须通过 interface 注入,构造函数里校验 nil
如果 Handler 或 Service 的构造函数接收 *sql.DB 或 redis.Client 这类具体类型,测试时就无法替换成 fake 实现——接口才是解耦的起点。
常见翻车点:
- 定义了
UserRepository接口,但构造函数写成func NewUserService(db *sql.DB),测试时传&fakeRepo{}直接编译报错 - 字段声明为
repo *UserRepoImpl而非repo UserRepository,失去多态能力,运行时 panic 位置模糊 - 构造函数没校验
if repo == nil { panic("repo is required") },测试一 mock 就崩在repo.GetUser()那行,看不出是传空了
手写 fake 比 gomock 更快更稳,别在 test 包里复用
gomock 配置稍有偏差(比如方法签名不一致、mockgen 路径错),测试就“假通过”:跑得过去,但实际没走你写的逻辑。而一个三行 fake 就能覆盖 90% 场景。
典型写法:
type fakeUserRepo struct {
CalledWithID int
ReturnUser *User
ReturnErr error
}
func (f *fakeUserRepo) GetUserByID(id int) (*User, error) {
f.CalledWithID = id
return f.ReturnUser, f.ReturnErr
}
测试后可断言:assert.Equal(t, 123, f.CalledWithID);模拟错误只需 ReturnErr: errors.New("timeout")。注意 fake 不要跨包复用,每个 test 文件自己写一个,避免状态污染。
main 函数是唯一合法的依赖组装点
所有依赖链必须从 main() 自底向上显式创建:先 new logger、config、DB,再 new repo,再 new service,最后传给 handler。任何一层漏传,panic 位置清晰,修复路径明确。
反模式示例:
- 在 handler 里直接
new(sql.DB)或读 config 文件——测试时无法替换,还得启动真实 DB - 用
map[string]interface{}传依赖——绕过类型检查,测试时你连该 mock 哪个字段都不知道 - 字段名用
svc或db这种模糊名——语义不清,测试意图难懂
真正难的不是写 fake,而是把 handler 里的业务逻辑抽出来、让参数变简单、把副作用推到最外层——否则测试永远卡在“没法测”。











