登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go httptest为测试客户端注入自定义 RoundTripper的实践示例

来源:17golang原创

时间:2026-09-15 22:52:13 132浏览 收藏

测试依赖 HTTP 服务时,httptest.NewServer 负责提供一个可关闭的本地服务,自定义 http.RoundTripper 则负责控制客户端发出的请求。两者可以叠加:让测试服务器返回真实的 HTTP 响应,同时在传输层记录请求;遇到重试、500 或网络错误分支时,也可以只注入一个假的传输器。

要点速览
  • Server.Client() 适合接入测试服务器,自定义 Transport 可以包裹它来观察请求。
  • RoundTrip 返回 HTTP 状态码不等于返回 error;500 是响应,无法拿到响应才是传输错误。
  • 每个测试都要关闭 httptest.Server 和 Response.Body,可复用的 Transport 还要考虑并发安全。

先分清 httptest.Server 与 RoundTripper 的职责

httptest.NewServer 启动的是本地 HTTP 服务,适合验证 URL、请求头、JSON 响应和处理器之间的真实链路;http.Client.Transport 位于客户端更低的一层,负责一次 HTTP 事务。自定义 RoundTripper 可以截获请求、改写测试用 Header,或直接返回预设响应,但它不应该替客户端处理重定向、Cookie 等高层逻辑。

因此,测试一个正常成功场景时可以使用两层组合;测试服务不可达、固定 500 或重试上限时,直接替换 Transport 会更快、更稳定。

用自定义 RoundTripper 包装 httptest.Server 的客户端

下面的例子让测试服务器返回 JSON,再用包装器记录请求方法、路径和 Header。包装器不伪造响应,而是把请求交给 Server.Client() 原本的 Transport。

type recordingTransport struct {
	next http.RoundTripper
	got  *http.Request
}

func (t *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	// Clone 只用于断言,避免测试结束后继续读取调用方可能复用的 Request。
	t.got = req.Clone(req.Context())
	return t.next.RoundTrip(req)
}

func TestClientWithRecordingTransport(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 处理器只验证服务端收到的请求,并返回稳定的测试数据。
		if r.Header.Get("X-Test-Mode") != "stub" {
			http.Error(w, "missing test header", http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		io.WriteString(w, `{"ok":true}`)
	}))
	defer server.Close() // 释放 httptest.Server 的监听资源。

	client := server.Client()
	base := client.Transport
	if base == nil {
		base = http.DefaultTransport
	}
	recorder := &recordingTransport{next: base}
	client.Transport = recorder

	req, err := http.NewRequest(http.MethodGet, server.URL+"/health", nil)
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("X-Test-Mode", "stub")
	resp, err := client.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close() // 读取完响应后关闭 Body,避免连接资源泄漏。
	if resp.StatusCode != http.StatusOK || recorder.got.URL.Path != "/health" {
		t.Fatalf("unexpected response or path: %d %s", resp.StatusCode, recorder.got.URL.Path)
	}
}
Go httptest NewServer、http.Client 与自定义 RoundTripper 记录请求并到达 Handler 的结构说明图
图1:结构说明图,展示 httptest.Server 客户端与自定义 RoundTripper 的分层关系。

这里的关键是保留原 Transport 的实际行为。如果把 next 留空,包装器就会在第一次请求时 panic;如果只想完全离线测试,则不要包裹真实 Transport,而应让自定义实现直接构造 http.Response。

用函数式 RoundTripper 注入固定响应或错误

函数式写法适合覆盖不容易稳定复现的分支。返回带 500 状态码的响应时,client.Do 通常仍会得到 resp,错误值为 nil;只有没有获得 HTTP 响应的情况,才应该返回传输错误。

type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	// 把测试用的请求交给闭包,便于按 URL 或 Header 选择响应。
	return f(req)
}

func TestClientHandlesRemoteFailure(t *testing.T) {
	client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
		// 500 是服务器已经给出的响应,不应伪装成网络 error。
		return &http.Response{
			StatusCode: http.StatusInternalServerError,
			Status:     "500 Internal Server Error",
			Body:       io.NopCloser(strings.NewReader(`{"error":"busy"}`)),
			Header:     make(http.Header),
			Request:    req,
		}, nil
	})}

	resp, err := client.Get("https://service.invalid/profile")
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close() // 即使断言失败,也要释放注入响应的 Body。
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("want 500, got %d", resp.StatusCode)
	}
}
Go roundTripFunc 区分固定 500 HTTP 响应与无法获得响应的传输错误的边界结构图
图2:边界结构图,区分固定 HTTP 响应与 RoundTrip 传输错误。

要模拟超时或连接失败,只需把闭包改为 return nil, context.DeadlineExceeded 或自定义 error。不要同时返回一个不完整的 Response 和 error;调用方容易在错误处理与 Body 关闭之间产生歧义。

收尾检查:响应体、Transport 与测试边界

检查点建议原因
本地服务每个 NewServer 都 defer Close()释放监听和连接资源
响应体成功拿到响应后关闭 Body避免连接不能复用或资源泄漏
包装器保留并复用底层 Transport不改变测试服务器的传输配置
并发测试不要把可变请求记录写入共享无锁字段RoundTripper 可能被并发调用

常见问题

为什么已经有 httptest.NewServer,还要写 RoundTripper?

NewServer验证真实 HTTP 链路,RoundTripper则能在客户端入口观察、改写或直接替换传输行为,关注点不同。

自定义 RoundTripper 返回 500 时,client.Do 会报错吗?

通常不会。500 是合法 HTTP 响应,调用方应先判断 error,再读取 StatusCode;网络失败才通常表现为 error。

能把同一个 recordingTransport 用在并发测试里吗?

只有在记录字段受互斥锁、channel 或其他同步方式保护时才可以,否则多个 RoundTrip 同时写入 got 会产生数据竞争。

相关官方文档:https://pkg.go.dev/net/http/httptest、https://pkg.go.dev/net/http#RoundTripper。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>