必须用 httptest.NewRecorder 捕获 Handler 响应,它能完整记录状态码、header 和 body;需配合 httptest.NewRequest 构造请求,注意路径/查询参数模拟、ParseForm 调用及中间件执行完整性。

怎么用 httptest.NewRecorder 捕获 Handler 的响应
Handler 测试的核心是绕过真实网络,把请求“喂”给 Handler,再看它写了什么到 ResponseWriter。httptest.NewRecorder 就是那个能记住所有写入内容的假 http.ResponseWriter。
常见错误是直接传 nil 或自定义空结构体,结果状态码、header、body 全丢——httptest.ResponseRecorder 才是唯一可靠的选择。
- 必须用
httptest.NewRequest构造请求,不能用http.Request{}字面量(缺少底层字段,调用ParseForm等会 panic) -
recorder.Code是状态码,recorder.Body.String()是响应体,recorder.Header()可查 header - 如果 Handler 里调用了
http.Redirect,记得检查recorder.Code是否为302,且recorder.Header().Get("Location")是否符合预期
如何测试带路径参数或查询参数的路由
Go 标准库的 http.ServeMux 不解析路径参数(如 /user/{id}),那是第三方路由器(gorilla/mux、chi)的事。测试时得按你实际用的路由方式来模拟。
如果你用的是 net/http 原生 mux,路径参数只能靠字符串切割或正则提取,测试时就老老实实拼完整路径;如果用了 gorilla/mux,就得用它的 Router.ServeHTTP,并确保在测试前用 router.HandleFunc 注册好带变量的路由。
立即学习“go语言免费学习笔记(深入)”;
- 查询参数:用
req.URL.RawQuery = "name=alice&age=30",或更安全地用url.Values{"name": {"alice"}, "age": {"30"}}.Encode() - 路径参数(
gorilla/mux):构造请求时路径写死(如/api/user/123),并在注册 handler 时用{id:\d+}这类 pattern - 别忘了调用
req.ParseForm()或req.ParseMultipartForm()—— 否则req.Form始终为空 map
为什么 recorder.Body.Bytes() 有时是空的
最常见原因是 Handler 没真正写响应:比如提前 return、panic 了、或者写了但被中间件拦截(比如日志中间件吞了 panic 却没写 status),又或者用了 io.Copy 但目标是 http.NoBody 这类空 reader。
另一个隐蔽坑是:Handler 内部调用了 http.Error 但没 return,后续代码继续执行并覆盖了状态码和 body。
- 在测试断言前加一行
log.Printf("status: %d, body: %q", recorder.Code, recorder.Body.String()),快速确认是否真没输出 - 检查 Handler 是否有未处理的 error 分支,尤其是 JSON 编码失败(
json.Marshal报错后没 return) - 如果用了中间件链,确保每个中间件都调用了
next.ServeHTTP,否则请求根本到不了你的 Handler
并发测试 Handler 时要注意什么
httptest 本身是线程安全的,但你的 Handler 如果依赖全局变量、共享 map、未加锁的 struct 字段,就会在并发测试中出问题——这和生产环境行为一致,只是测试时更容易暴露。
比如用 map[string]string{} 当内存缓存,又没加 sync.RWMutex,并发读写直接 panic。
- 测试并发请用
go test -race,它能捕获大部分数据竞争 - 避免在 Handler 里操作包级变量;必须共享状态时,优先用
sync.Map或带锁封装 -
httptest.NewServer虽然方便,但在并发测试中开太多临时 server 会耗尽端口或 fd,纯httptest.NewRecorder更轻量可控
Handler 测试真正的复杂点不在工具链,而在于你是否清楚自己的中间件顺序、路由匹配逻辑、以及 error 处理是否真的终止了响应流程——这些地方一漏,测试就变成“看起来过了,其实没测对”。










