http.Error 不适合错误注入测试,因其会提前终止 handler,跳过中间件、日志和指标等真实链路逻辑;应通过 mock 依赖客户端(如 httptest.Server)在下游层精准注入错误。
为什么 http.Error 不适合做错误注入测试
直接用 http.error 返回 500 或伪造响应体,会让 handler 提前退出,中间件、日志、指标埋点等逻辑全被跳过——你测的不是真实调用链,只是个“假失败”。真正要验证的是:当下游 api(比如依赖的 userservice)返回 404 或超时,你的服务是否按预期降级、重试或返回兜底数据。
实操建议:
- 不要在 handler 内部手动调用
http.Error模拟错误,而是控制依赖的 HTTP 客户端行为 - 把外部 API 调用封装成可替换的接口(例如
UserClient),测试时注入 mock 实现 - 避免在测试中 patch 全局
http.DefaultClient,它会影响并发测试且难以隔离
用 httptest.Server 模拟不稳定下游 API
这是最可控、零依赖、符合 Go 原生风格的做法。你不需要引入第三方 mock 库,httptest.Server 可以精准返回任意状态码、延迟、甚至随机中断连接。
示例场景:测试你的服务调用 GET /v1/user/123 时,下游突然返回 503:
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(503)
w.Write([]byte(`{"error":"service_unavailable"}`))
}))
ts.Start()
defer ts.Close()
// 注入到你的 client(比如 config.APIBase = ts.URL)
关键点:
-
NewUnstartedServer允许你在启动前设置Config.ReadTimeout等,模拟网络超时 - 用
ts.Close()而非ts.CloseClientConnections(),避免残留连接干扰下个测试 - 如果要测试重试逻辑,让 handler 在前两次返回 503、第三次返回 200,用闭包计数器即可
如何让错误注入可配置、不污染生产代码
硬编码错误行为会污染业务逻辑。推荐用函数选项(functional options)+ 接口抽象,让错误注入只存在于测试文件中。
定义客户端接口:
type UserClient interface {
GetUser(ctx context.Context, id string) (*User, error)
}
实现时支持传入错误策略:
func NewUserClient(baseURL string, opts ...UserClientOption) *userClient {
c := &userClient{client: http.DefaultClient, baseURL: baseURL}
for _, opt := range opts {
opt(c)
}
return c
}
type UserClientOption func(*userClient)
func WithErrorInjection(statusCode int, body string) UserClientOption {
return func(c *userClient) {
c.injectStatus = statusCode
c.injectBody = body
}
}
这样测试里就能写:
client := NewUserClient("https://fake", WithErrorInjection(429, `{"msg":"rate_limited"}`))
注意:
- 生产代码完全不引用
WithErrorInjection,编译期零开销 - 不要把错误注入逻辑塞进 struct 字段(如
ShouldFail bool),容易误触发 - 如果多个测试共用同一 client 实例,需确保 goroutine 安全,建议每个 test case 新建 client
常见踩坑:Context 超时与错误注入的时序冲突
很多同学注入了 500,但测试断言失败,实际是因为 handler 的 ctx 先超时了,根本没走到下游调用——你看到的 “timeout” 是你自己的上下文,不是下游错误。
排查步骤:
- 检查 handler 是否用了
context.WithTimeout(req.Context(), 100*time.Millisecond)这类短超时 - 测试中构造的
req必须带足够长的 context:req = req.WithContext(context.WithTimeout(context.Background(), 5*time.Second)) - 若用
http.Client.Timeout控制下游超时,确保它比 handler context 更短,否则无法触发重试逻辑 - 打印
err的具体类型:*url.Error是网络层问题,*json.UnmarshalError是解析失败,二者处理路径完全不同
错误注入真正难的不是造错,而是让错发生在正确的位置、被正确的逻辑捕获。别急着写 mock,先理清你到底想验证哪一层的健壮性:是 HTTP transport 层?JSON 解析层?还是业务决策层?每层对应的注入点和断言方式都不同。


















