模拟 HTTP 响应有三种方式:①用 httptest.NewRecorder 测试 handler;②用 httpmock/gock 拦截真实请求;③手动构造 *http.Response。核心是确保 Body 为可重放的 io.ReadCloser,避免 EOF 或 panic。

Go 标准库的 http.Response 本身不可直接构造(字段全为只读),所以“模拟响应”实际是指在测试中绕过真实 HTTP 请求,用可控的输入替代网络调用——核心是替换 http.Client 的传输层或直接构造 *http.Response。
用 httptest.NewRecorder() 模拟 Handler 响应
这是最常见、也最轻量的模拟方式,适用于测试你自己的 HTTP handler 函数(如 func(http.ResponseWriter, *http.Request))。
它不发起请求,而是把 handler 的输出写入内存缓冲区,你可以断言状态码、头信息和响应体:
req, _ := http.NewRequest("GET", "/api/user", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(yourHandlerFunc)
handler.ServeHTTP(rr, req)
// 断言
if rr.Code != http.StatusOK {
t.Errorf("expected status %d; got %d", http.StatusOK, rr.Code)
}
if body := rr.Body.String(); !strings.Contains(body, `"id":1`) {
t.Errorf("unexpected response body: %s", body)
}
- 仅适用于测试 handler,不能用于测试依赖
http.Client的服务层代码 -
rr.Body是*bytes.Buffer,可多次读取;但rr.Result()返回的*http.Response中Body是一次性读取流,需注意重用逻辑 - 不会触发中间件(除非你显式包装 handler),若用 Gin/echo 等框架,应使用其对应测试工具(如
gin.CreateTestContext)
用 httpmock 或 gock 拦截真实 HTTP 请求
当你的代码内部调用了 http.DefaultClient.Do() 或自定义 *http.Client,需要拦截外发请求并返回伪造响应时,推荐用 httpmock(更 Go 风格)或 gock(API 更简洁)。
立即学习“go语言免费学习笔记(深入)”;
以 httpmock 为例:
func TestFetchUser(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", "https://api.example.com/user/123",
httpmock.NewStringResponder(200, `{"id":123,"name":"alice"}`))
resp, err := fetchUserFromAPI("123") // 内部调用 http.DefaultClient.Do
if err != nil {
t.Fatal(err)
}
// resp 是真实的 *http.Response,Body 可读取
}
- 必须在测试前后调用
Activate()和DeactivateAndReset(),否则会影响其他测试 - 默认只拦截
http.DefaultClient;若使用自定义*http.Client,需手动设置其Transport:client.Transport = httpmock.DefaultTransport - 响应体字符串会被自动封装为
io.ReadCloser,但要注意:若 handler 中多次读取Body(如先解析 JSON 再记录日志),需用httpmock.NewResonder+io.NopCloser(bytes.NewReader(...))手动构造可重放 Body
手动构造 *http.Response(极简场景)
某些单元测试中,你只需要一个“长得像响应”的对象(比如传给某个解析函数),且不关心传输细节,可跳过网络层直接构造。虽然 http.Response 字段不可写,但可通过 http.Response{...} 字面量初始化(所有字段均为导出,只是无 setter):
resp := &http.Response{
StatusCode: 200,
Status: "200 OK",
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"ok":true}`)),
}
resp.Header.Set("Content-Type", "application/json")
-
Body必须是io.ReadCloser,io.NopCloser是最简包装方式;漏掉会导致 panic:“Body not an io.ReadCloser” -
Request字段可为nil,但若被测逻辑访问了resp.Request.URL等字段,需补全Request: &http.Request{URL: &url.URL{Scheme: "https", Host: "x.y", Path: "/z"}}) - 这种方式绕过了所有 HTTP 协议逻辑(如重定向、gzip 解压),仅适合纯数据解析类测试
真正容易被忽略的是 Body 生命周期:无论用哪种方式模拟,只要涉及多次读取或并发读取,就必须确保 Body 支持重放(如用 bytes.Buffer 包装)或提前读取缓存;否则测试会因 EOF 或 panic 失败,而线上行为可能恰好掩盖这个问题。










