单元测试应避免启动真实http服务器,而用httptest.newrequest和httptest.newrecorder配合router.servehttp进行纯函数式测试,显式注册中间件、使用c.shouldbindjson、禁用默认中间件以确保可控性和准确性。

测试时别起真实 HTTP 服务器
单元测试里调用 router.Run() 或监听端口,等于在跑集成测试——慢、依赖网络、难定位问题。Gin 的 handler 是纯函数,完全能脱离 net/http 运行。
正确做法是用 httptest.NewRequest 构造请求,httptest.NewRecorder 捕获响应,再手动触发 router.ServeHTTP(w, req)。
- 带 JSON body?用
strings.NewReader(`{"id":1}`)包一层传给http.NewRequest - URL 参数(如
/user/:id)?路径直接写死值,比如"/user/42",c.Param("id")会自动解析 - Query 参数(如
?page=2&size=10)?拼进路径,或显式设置req.URL.RawQuery = "page=2&size=10"
中间件不会自动生效,必须显式注册
你在 main.go 里写的 r.Use(authMiddleware),对测试文件中新建的 gin.New() router 完全无效。测试上下文是干净的,不继承任何全局配置。
漏掉中间件最常导致两类误判:
- 线上返回 401,但测试通过(因为没走 auth)
- 测试里报 404/400,但线上正常(比如 CORS 中间件没注册,测试时跨域被拦)
建议把路由初始化逻辑抽成函数,例如:func NewRouter(mws ...gin.HandlerFunc) *gin.Engine,测试时传入 mock 版本的中间件(如固定 token 的 JWT 验证)。
c.BindJSON() 和 c.ShouldBindJSON() 别混用
这是 Gin 测试里最隐蔽的坑:c.BindJSON(&v) 一旦解析失败(字段类型错、必填字段缺失),会直接写 400 响应并中断 handler;你若只检查 w.Body.String() 却忽略 w.Code,就会误判为“空响应但成功”。
c.ShouldBindJSON(&v) 只校验、不自动响应,把控制权交还给你——这才是单元测试需要的可控行为。
- handler 里优先用
c.ShouldBindJSON(),自己决定错误怎么处理(比如统一返回格式) - 测试时主动构造非法 JSON(如字符串字段传数字),验证是否返回
http.StatusBadRequest - 若必须用
BindJSON,断言里必须包含w.Code == http.StatusBadRequest
别依赖 gin.Default() 的默认中间件
gin.Default() 自动加了 Logger() 和 Recovery(),但在测试中它们可能干扰断言:日志写进 w.Body、Recovery 捕获 panic 后返回 500 而非原始 panic,掩盖真实问题。
测试应尽量用 gin.New(),按需注册中间件。如果要用 Default(),至少设 gin.SetMode(gin.TestMode) ——它只影响日志输出和 panic 处理方式,不影响中间件注册与执行逻辑。
真正容易被忽略的是:中间件顺序、绑定逻辑(如 c.ShouldBind 的时机)、以及 handler 内部对 c.Next() 的依赖——这些都得在测试里显式模拟,不能靠“看起来跑通了”就认为没问题。











