必须设gin.TestMode,否则gin.Default()自动挂载Logger和Recovery中间件,导致日志刷屏、重定向触发、panic被捕获而无法断言错误路径;正确做法是在TestMain中调用gin.SetMode(gin.TestMode)或每个测试开头设置。

必须设 gin.TestMode,否则中间件和日志会干扰断言
不设测试模式时,gin.Default() 会自动挂载 Logger 和 Recovery 中间件,导致控制台刷屏、重定向被触发、panic 被捕获后无法断言错误路径。这不是 bug,是调试模式的默认行为。
正确做法是在所有测试开始前统一设置:
- 在
TestMain函数中调用gin.SetMode(gin.TestMode),然后m.Run() - 或每个测试函数开头加
gin.SetMode(gin.TestMode)(适合单测隔离强的场景) - 避免混用
gin.Default()和gin.New():前者自带中间件,后者裸机,选哪个取决于你是否要测中间件本身
构造请求必须匹配 Gin 的解析路径:URL 参数、Query、Header、Body 各有规矩
Gin 不会自动从 URL 或 header 里猜字段——它严格按调用方式取值。错一个地方,c.Param()、c.Query()、c.GetHeader() 或 c.ShouldBindJSON() 就返回零值或报错。
- URL 参数(如
/users/:id):请求路径必须写成"/users/123",不能写成"/users?id=123" - Query 参数(如
c.Query("name")):拼在 URL 后,例如"/users/123?name=alice&role=admin" - JSON Body:必须设
req.Header.Set("Content-Type", "application/json"),否则c.ShouldBindJSON()直接返回err != nil - Form 表单或文件上传:绝不能传 JSON 字符串;必须用
bytes.Buffer + multipart.Writer构造,且Content-Type头必须等于writer.FormDataContentType()
c.FormFile() 返回 nil?90% 是请求体没构造对或没走解析流程
这个 nil 不代表业务逻辑错了,而是 Gin 根本没成功解析出 multipart 表单。根本原因是:c.MultipartForm()(被 c.FormFile() 内部调用)只在首次访问时解析一次 body,且依赖完整的 boundary 结构和合法的字段名。
- 漏掉
writer.Close()→ body 缺少 final boundary → 解析失败 →FormFile返回 nil - 字段名写错,比如接口期待
"avatar",但测试里写了CreateFormFile("file", ...) - 中间件提前读了
c.Request.Body(比如鉴权中间件解析 JWT payload),又没重放,导致后续FormFile拿到空流 - 没在 handler 开头加
fmt.Printf("form: %+v", c.MultipartForm())验证是否为 nil,就直接断言业务逻辑,容易误判
比对响应别用 assert.Equal,优先用 assert.JSONEq
Go 的 map 序列化顺序不固定,c.JSON() 输出的 key 顺序每次可能不同。用 assert.Equal(t, `{"a":1,"b":2}`, w.Body.String()) 极易因字段顺序抖动而失败,不是逻辑问题,是比对方式错了。
-
assert.JSONEq会先解析两边 JSON 成结构体再递归比对,完全容忍空格、换行、key 顺序差异 - 响应体含二进制内容(如图片、PDF)时,别调
w.Body.String(),改用w.Body.Bytes()配合bytes.Equal - HTTP 状态码、Header 字段(如
"Content-Type")必须单独断言,JSONEq不覆盖这些
实际写测试时,最常被跳过的其实是「验证 multipart 是否真的被初始化」和「确认中间件是否参与了请求生命周期」——这两点不显式检查,很多 nil 和 401 就成了玄学问题。











