测试前必须设gin.testmode,否则gin.default()启用logger和recovery中间件导致日志刷屏、panic被吞、响应体混入调试信息;须用router.servehttp而非router.run(),通过httptest.newrequest和newrecorder内存级验证,并用assert.jsoneq比对json响应。

测试前必须设 gin.TestMode
不设这个模式,gin.Default() 会启用 Logger() 和 Recovery() 中间件,导致日志刷屏、panic 被吞掉、响应体混入调试信息,断言直接失败。
- 在
TestMain里统一设置最稳妥:func TestMain(m *testing.M) { gin.SetMode(gin.TestMode); os.Exit(m.Run()) } - 如果没写
TestMain,每个测试函数开头都得加gin.SetMode(gin.TestMode) - 别信“只测 handler 不走中间件就不用设”——只要用了
gin.Default(),默认中间件就生效,哪怕你没显式注册路由
router.ServeHTTP 是核心,别起真实服务器
调 router.Run() 或 httptest.NewServer() 是集成测试,慢、依赖端口、难 debug,还可能被防火墙/端口占用干扰。单元测试必须走内存路径。
- 用
http.NewRequest(method, url, body)构造请求,body是io.Reader,JSON 就用strings.NewReader(`{"k":"v"}`) - 用
httptest.NewRecorder()捕获响应,它实现了http.ResponseWriter接口 - 关键一步:
router.ServeHTTP(w, req)—— 这才是触发整个 Gin 请求链的唯一正确入口 - 路径含 URL 参数(如
/users/:id)时,直接写死值:"/users/123",Gin 会自动解析进c.Param("id")
JSON 响应比对必须用 assert.JSONEq
assert.Equal(t, `{"a":1,"b":2}`, w.Body.String()) 极易失败:Go map 序列化 key 顺序不确定,空格换行也敏感。这不是逻辑错,是比对方式错。
- 一律改用
assert.JSONEq(t, `{"a":1,"b":2}`, w.Body.String()),它解析后按结构比对,容忍格式抖动 - 如果 handler 返回二进制或非 UTF-8 内容(比如
c.Data),别用w.Body.String(),改用w.Body.Bytes()配合bytes.Equal - 断言前务必检查状态码:
assert.Equal(t, http.StatusOK, w.Code),否则 400 错误可能被当成“成功返回空 JSON”漏掉
中间件测试要分合法/非法两种请求头
中间件不是装饰器开关——它只在请求匹配且注册了才执行。测试时漏掉 header 或格式不对,中间件根本不会触发,结果 200 成功纯属假阳性。
- 测 Auth 中间件:合法请求必须带
req.Header.Set("Authorization", "Bearer xxx"),断言w.Code == http.StatusOK - 非法请求:不设 header,或设成
"token xxx"(少 Bearer)、"Bearer"(缺 token),断言w.Code == http.StatusUnauthorized - 中间件依赖外部服务(如 Redis 查 token),必须抽象成接口 + fake 实现,否则测试变集成测试,CI 会随机失败
gin.Engine 实例,不继承任何全局配置,连中间件都要手动注册。漏掉一行 router.Use(authMw) 或错一个 header,测试就和线上行为对不上。











