Gin单元测试核心是三步:构造请求、调用handler、断言响应;需正确注册路由、用httptest.NewRequest传完整URL(含路径参数和查询参数)、设置Content-Type等头,再通过assert验证状态码和JSON响应。

直接上手写 Gin 单元测试,别被“TestMode”“httptest”这些词绕晕——核心就三件事:构造请求、调用 handler、断言响应。其他都是围绕这三点的细节补丁。
怎么让 c.Param() 和 c.Query() 在测试里正常工作
Gin 不会自动从 URL 字符串里解析路径参数或查询参数,得靠 httptest.NewRequest 构造带完整路径的请求,并确保路由已注册。
- URL 路径必须和路由定义完全一致,比如
router.GET("/users/:id", handler),测试请求就得是http.NewRequest("GET", "/users/123?name=foo", nil) - 不能只传
"123"给c.Param("id")—— Gin 是在匹配路由时才把路径段塞进 context 的,没走路由匹配就拿不到 - 如果用了分组路由(如
api := router.Group("/api")),测试 URL 也得带上前缀,比如/api/users/123 -
c.Query("name")只读 URL 查询字符串,不读表单或 JSON body;测试时别把参数塞错地方
为什么 c.FormFile("file") 在测试里总返回 nil
不是 Gin 有问题,是 multipart 请求体根本没构造对。Gin 的 c.FormFile 依赖底层 http.Request.MultipartForm,而它只在满足严格条件时才被初始化。
- 必须用
bytes.Buffer+multipart.Writer构建请求体,不能拼字符串、不能用strings.NewReader - 务必调用
writer.Close(),否则 boundary 结束符缺失,ParseMultipartForm直接失败 -
req.Header.Set("Content-Type", writer.FormDataContentType())必须执行,且值必须和writer生成的一致 —— 手动写死multipart/form-data; boundary=xxx极易出错 - 字段名(如
"file")要和 handler 里c.FormFile("file")的参数完全一致,大小写敏感
如何测中间件是否生效,比如鉴权或日志
中间件本质是函数链,测试的关键是把它挂到路由上,再看最终响应是否符合预期行为。
- 测试鉴权中间件时,记得手动设置 header:
req.Header.Set("Authorization", "Bearer xxx"),否则中间件大概率直接c.AbortWithStatusJSON(401, ...) - 如果中间件修改了
c.Next()后的响应(比如加 header、改 status),要在router.ServeHTTP(w, req)后检查w.Code或w.Header() - 避免在中间件里提前读取
c.Request.Body(比如日志中间件调了c.GetRawData()),否则后续c.FormFile()会因 body 已关闭而失败 - 想验证中间件是否被调用,可以在里面加个可观察的副作用,比如往
c.Set("middleware_called", true),然后在 handler 里检查
测试覆盖率低?先盯住 c.MultipartForm() 和 c.ShouldBind* 的分支
业务逻辑常藏在这两个地方:文件校验(大小、类型、空文件)、结构体绑定失败处理。这些分支容易漏测,但线上最容易出问题。
- 空文件场景:用
writer.CreateFormFile("file", "empty.txt")创建字段,但不写内容,c.FormFile()仍返回非 nil 的*multipart.FileHeader,文件内容长度为 0 - 超限文件:不用真传大文件,mock
c.Request.ContentLength或在 handler 里提前判断req.ContentLength > 10 -
c.ShouldBindJSON()失败时,c.Errors会被填充,测试要覆盖err != nil分支,别只测成功流 - 别依赖
go test -cover报告里的百分比数字,重点看 handler 函数里 if/else 分支有没有被触发 —— 尤其是 error return 路径











