应使用assert.JSONEq而非assert.Equal比对JSON响应体,因其解析后语义比对,忽略字段顺序、空白、浮点精度及null/nil差异;须设gin.TestMode、正确设置Content-Type头、确保路由注册并匹配路径参数。

用 assert.JSONEq 比对响应体,别用字符串相等
直接用 assert.Equal(t, `{"code":0}`, w.Body.String()) 极易失败:JSON 字段顺序不保证、空格/换行差异、浮点数精度、null 和 nil 序列化表现不同。Gin 返回的 JSON 是由 json.Marshal 生成的,字段顺序取决于结构体定义顺序或 map 迭代顺序(Go 1.12+ 随机),不是字典序。
正确做法是用第三方断言库(如 github.com/stretchr/testify/assert)提供的 assert.JSONEq:
assert.JSONEq(t, `{"code":0,"data":{"id":123}}`, w.Body.String())
- 它先解析两边为
map[string]interface{},再递归比对键值,忽略顺序和空白 - 支持嵌套结构、数组、
null与 Go 的nil自动对齐 - 报错时会高亮具体差异,比如
"data.id" expected 123, got 456
必须设 gin.TestMode,否则中间件干扰断言
漏掉 gin.SetMode(gin.TestMode) 是最常被忽略的坑。默认 gin.Default() 启用 gin.Logger() 和 gin.Recovery(),它们会在测试输出里刷屏日志、修改响应头、甚至捕获 panic 后返回 HTML 错误页——导致你拿到的 w.Body.String() 是 HTML 而非 JSON,断言必然失败。
两种安全写法:
- 在每个测试函数开头加:
gin.SetMode(gin.TestMode) - 或在
TestMain中统一设置(推荐):
func TestMain(m *testing.M) {
gin.SetMode(gin.TestMode)
os.Exit(m.Run())
}
POST/PUT 请求必须显式设 Content-Type 头
调用 c.ShouldBindJSON() 时,Gin 会检查请求头 Content-Type 是否为 application/json。测试中若只构造 httptest.NewRequest("POST", "/api", bytes.NewReader(jsonBytes)) 而不设头,绑定会静默失败(返回 err != nil),但很多人没检查 err 就继续执行,结果业务逻辑用零值处理,返回的 JSON 数据错乱。
务必补上这一行:
req.Header.Set("Content-Type", "application/json")
- GET 请求无需设,因为没请求体
- 表单提交用
application/x-www-form-urlencoded,对应c.ShouldBind()或c.ShouldBindWith(&v, binding.Form) - 设错类型(比如 POST JSON 却设成
text/plain)会导致绑定失败且无提示
路由未注册导致 404,不是 handler 逻辑问题
测试时看到 404 响应,第一反应不该是“handler 写错了”,而应检查路由是否注册。Gin 的 router.ServeHTTP() 是纯内存转发,如果 router.GET("/users", handler) 没执行,请求根本到不了你的 handler,w.Code 就是 404,w.Body 是默认错误页。
验证方式:
- 打印
router.Routes()看注册列表(调试时临时加) - 确保测试用的
*gin.Engine实例和实际启动服务的是同一个(别在测试里 new 一个没注册路由的 router) - 路径参数要匹配:注册的是
/users/:id,测试 URL 就得是/users/123,不能写/users?id=123
复杂点在于中间件和分组路由——如果你的 handler 在 v1 := r.Group("/v1") 下注册,测试请求 URL 必须带 /v1/xxx,否则照样 404。











