单元测试中不应调用 router.Run(),而应使用 httptest.NewRequest 和 httptest.NewRecorder 手动调用 router.ServeHTTP(w, req),并显式注册中间件、避免 gin.Default()、优先使用 c.ShouldBindJSON()。

别在单元测试里调 router.Run()
单元测试里起真实 HTTP 服务器(比如调 router.Run() 或监听端口)是错的——这不是单元测试,是集成测试,慢、不稳定、难 debug。Gin 的 handler 完全可以脱离网络栈运行。
正确做法是用 httptest.NewRequest 构造请求,用 httptest.NewRecorder 捕获响应,再手动调 router.ServeHTTP(w, req):
- JSON body?用
strings.NewReader(`{"id":1}`)包一层传给http.NewRequest - 路径参数(如
/user/:id)?直接写死路径,比如"GET", "/user/123", nil,Gin 会自动解析c.Param("id") - Query 参数(如
?page=2)?拼进路径,或手动设req.URL.RawQuery = "page=2" - 别忘了
import "net/http/httptest"和"net/http"
中间件不会自动生效,必须显式注册
你在 main.go 里写的 r.Use(authMiddleware),对测试文件里新 gin.New() 出来的 router 完全无效。测试上下文是干净的,不继承任何全局配置。
漏掉中间件最常导致两种失败:
立即学习“go语言免费学习笔记(深入)”;
- 线上返回 401,但测试通过(因为没走 auth)
- 测试里返回 404/400,但线上正常(比如 CORS 中间件没注册,测试时跨域被拦)
建议把 router 初始化逻辑抽成函数,例如:func NewRouter(mws ...gin.HandlerFunc) *gin.Engine,测试时传 mock 版 JWT 验证中间件,固定 token + mock 签名验证逻辑,别连真实密钥服务。
c.BindJSON() 和 c.ShouldBindJSON() 行为完全不同
这是 Gin 测试里最隐蔽的坑:
-
c.BindJSON(&v):一旦 JSON 解析失败(字段类型错、必填字段缺失),Gin 会直接写 400 响应并中断 handler 执行 -
c.ShouldBindJSON(&v):只校验、不自动响应,把控制权交还给你——这才是单元测试需要的可控行为
handler 里优先用 c.ShouldBindJSON(),自己决定怎么处理错误;测试时主动构造非法 JSON(如字符串字段传数字),验证是否返回 400 而非 200 + 空体。若必须用 BindJSON,断言里必须检查 w.Code == http.StatusBadRequest。
别依赖 gin.Default() 的默认中间件
gin.Default() 自动加了 Logger() 和 Recovery(),但在测试中它们可能干扰断言(比如日志写到 w.Body)或掩盖真实 panic(Recovery 捕获后返回空响应)。
单元测试应该用 gin.New() 启动干净 router,按需注册中间件。如果要测 Recovery 行为,得手动触发 panic 并验证它是否被捕获;如果只是测业务逻辑,跳过它更安全、更可控。
真正容易被忽略的是:中间件注册顺序、c.MustGet() 依赖注入是否在测试中被模拟、以及 gin.TestMode 只影响日志和 panic 处理,不影响中间件执行——别把它当成“测试开关”。


















