测试中应通过c.Errors.Last()获取handler调用后注入的业务错误,用errors.As()安全断言自定义错误类型及字段,而非检查响应体或字符串匹配。

测试中怎么拿到 handler 抛出的业务错误
Gin 的 c.Error() 不会自动终止流程,也不会写响应体,它只是把错误塞进 c.Errors 切片。所以单元测试里不能靠检查 HTTP 响应体内容来断言业务错误——那里面压根没你塞进去的 *AppError。
正确做法是:用 gin.CreateTestContext() 构造上下文,调用 handler 后直接读取 c.Errors,再做类型断言或错误匹配。
- 必须显式调用
c.Next()或直接执行 handler 函数体(不走路由匹配) -
c.Errors是个gin.Errors类型(底层是[]error),取最后一个最常用:c.Errors.Last() - 别用
err.Error()字符串匹配,脆弱且难维护;优先用errors.As(err, &target)或类型断言
如何断言自定义错误类型和字段
假设你定义了 type AppError struct { Code int; Message string; HTTPCode int },并在 handler 里写了 c.Error(&AppError{Code: 1001, Message: "user not found", HTTPCode: 404}),测试时就得还原这个结构。
示例断言逻辑:
func TestUserHandler(t *testing.T) {
r := gin.New()
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("GET", "/user/0", nil)
// 手动调用 handler(不走 r.GET)
userHandler(c)
var appErr *AppError
if assert.True(t, errors.As(c.Errors.Last(), &appErr)) {
assert.Equal(t, 1001, appErr.Code)
assert.Equal(t, "user not found", appErr.Message)
assert.Equal(t, 404, appErr.HTTPCode)
}
}
- 用
errors.As()安全提取指针,比err.(*AppError)更健壮(支持嵌套包装) - 如果 handler 里用了
c.AbortWithError(),它内部会调用c.Error(),同样可被c.Errors捕获 - 避免在测试里 mock
http.ResponseWriter去抓 JSON 响应——那属于「错误处理中间件」的职责,不是 handler 本体该暴露的
为什么 c.JSON(400, ...) 在测试里不好断言
直接在 handler 里写 c.JSON(400, gin.H{"message": "xxx"}) 看似简单,但会导致三个问题:
- 违反 Gin 错误处理正交原则:业务错误该走
c.Error()+ 统一中间件,而不是分散写响应 - 测试时得解析响应 body、反序列化 JSON、再比对字段,代码冗长且易因格式微调失败
- 无法区分是参数校验失败、DB 查询失败还是权限拒绝——所有都混在同一个 JSON 结构里,不利于错误分类统计
真正要测的是「handler 是否按预期注入了哪种错误」,不是「它返回的 JSON 长什么样」。
panic 错误在测试里怎么捕获
如果你的 handler 里用了 panic(&AppError{...}),默认的 gin.CreateTestContext() 不会 recover 它,测试会直接 panic 掉。
必须手动加一层 defer/recover 包裹 handler 调用:
func TestPanicHandler(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("GET", "/panic", nil)
var recovered interface{}
func() {
defer func() { recovered = recover() }()
panicHandler(c)
}()
var appErr *AppError
if assert.NotNil(t, recovered) {
assert.True(t, errors.As(recovered.(error), &appErr))
assert.Equal(t, 500, appErr.HTTPCode)
}
}
- 注意
recovered是interface{},需先断言为error再用errors.As - 这种测试只适用于明确走 panic 路径的场景;生产环境更推荐统一用
c.Error(),测试成本更低 - 别忘了:真实请求中 panic 必须由你写的
PanicRecovery()中间件捕获,测试里模拟的就是那个 recover 行为
真正难的不是写断言,而是让 handler 本身只负责「判断错不错」,不负责「怎么报错」——错误构造和响应格式必须剥离到中间件层,否则每个测试都要重复解析 JSON 或 mock writer。











