bigywater.tech
Go · 91

httptest:不开端口测试 Handler 的两种方式

测 HTTP handler,可以直接调用(ResponseRecorder),也可以起一个真的服务器(httptest.Server)。两者看到的请求、返回的响应并不一样,有些 bug 只在其中一种里出现。这篇并排比较,并把 Go 1.27.1 里新的内存网络版 NewTestServer 也跑了一遍。

1509 / 13320 / 45998 ns
Recorder、NewTestServer(内存网络)、NewServer(回环端口)各自的单次耗时
Content-Length
Recorder 没有,真实服务器有(100)
panic
Recorder 把 handler 的 panic 传进测试,真实服务器会恢复它

响应头不同

实测 · 本机执行输出
  ResponseRecorder:Code=200 Proto=HTTP/1.1 Content-Length="" Content-Type="text/plain; charset=utf-8" 响应头个数=1
  httptest.Server:Code=200 Proto=HTTP/1.1 Content-Length="100" Content-Type="text/plain; charset=utf-8" 响应头个数=3

同一个 handler,Recorder 的响应只有 Content-Type,真实服务器多了 Content-Length: 100 和另一个头(应当是 Date,没有逐个打印)。检查响应头的测试要想清楚用哪种。

请求不同

实测 · 本机执行输出
  NewRequest + Recorder:RemoteAddr=192.0.2.1:1234 Host=example.com Proto=HTTP/1.1 TLS=false BodyNil=false
  httptest.Server:RemoteAddr=127.0.0.1:58998 Host=127.0.0.1:58997 Proto=HTTP/1.1 TLS=false BodyNil=false
  httptest.NewRequest 的 Body == http.NoBody:true;自己 http.NewRequest 不给 body 时 Body 是 <nil>

httptest.NewRequest 造出来的请求 RemoteAddr 是 192.0.2.1:1234、Host 是 example.com,Body 是 http.NoBody;真实服务器看到的是实际的回环地址和端口。依赖 RemoteAddr 的代码(限流、日志)两种方式下取值不同。

静默与严格

实测 · 本机执行输出
  Recorder:WriteHeader(200) 再 WriteHeader(500) 之后 Code = 200,Body = "x",没有任何错误或日志

Recorder 不会提示重复 WriteHeader,Code 就是第一次的值,没有任何日志;真实服务器会在日志里打印 superfluous。

panic

实测 · 本机执行输出
  Recorder:handler 里的 panic 直接传给了测试代码(没有被 recover):handler panic
  httptest.Server:handler panic 时客户端得到 err = Get "http://127.0.0.1:58999": EOF

Recorder 直接调用 handler,panic 会传进测试代码,测试会因此失败;真实服务器会 recover 并关掉连接,客户端只看到 EOF。如果你的 handler 依赖“panic 会被服务器兜住”,用 Recorder 测不出来。

参数错误会 panic

实测 · 本机执行输出
  httptest.NewRequest("GET", "://bad", nil) → panic:invalid NewRequest arguments; parse "://bad": missing protocol scheme
  http.NewRequest("GET", "://bad", nil) → 返回 error:parse "://bad": missing protocol scheme

httptest.NewRequest 遇到非法 URL 直接 panic(测试代码里很方便),http.NewRequest 返回 error。

NewTestServer

实测 · 本机执行输出
  httptest.NewTestServer:ts.URL = "http://example.com";ts.Listener == nil:true;handler 看到 RemoteAddr=192.0.2.1:10001 Host=example.com Proto=HTTP/1.1 TLS=false BodyNil=false
  用 ts.Client() 访问任意地址 http://anything.invalid/other/path 也能到达这个服务器:状态 200,handler 看到 RemoteAddr=192.0.2.1:10002 Host=anything.invalid Proto=HTTP/1.1 TLS=false BodyNil=false

Go 1.27.1 里的 httptest.NewTestServer(t, handler) 默认用内存网络:没有真实端口(Listener 为 nil),ts.URL 是 http://example.com,必须用 ts.Client() 发请求,请求任意地址都会到达它(实验里用 anything.invalid 也行)。我没有查它是哪个 Go 版本引入的。

速度

实测 · 本机执行输出
  Recorder 直接调用 handler:1509 ns/次(20 次分配);httptest.NewServer(真实回环端口)+ 客户端往返:45998 ns/次(63 次分配);httptest.NewTestServer(内存网络)+ ts.Client() 往返:13320 ns/次(63 次分配)

Recorder 约 1.5 µs,内存网络的 NewTestServer 往返约 13.3 µs,真实回环端口约 46 µs。需要经过完整 HTTP 往返但不想占端口、也想快一点时,NewTestServer 比 NewServer 快三倍多。

文档

实测 · 本机执行输出
  NewTestServer returns a new Server for a test. The server will use an in-memory network implementation by default. If the handler is nil, the server will serve 500 responses to all requests. It will not use http.DefaultServeMux.
  NewRequest wraps NewRequestWithContext using context.Background.

文档说 NewTestServer 在 handler 为 nil 时对所有请求返回 500,并且不会使用 http.DefaultServeMux。

分步演示

httptest
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。每个对比是单独的小程序;速度用 testing.Benchmark;NewTestServer 需要 testing.TB,实验里用的是 benchmark 的 *testing.B;“用任意地址也能到达”实验用 anything.invalid 这个保留域名;文档摘自 go doc net/http/httptest
    • 没有验证:NewTLSServer 与 EnableHTTP2、NewTestServer 在 testing/synctest 下的表现、内存网络对 net.Conn 超时的支持、Recorder 的 Result() 对 trailers 的处理、NewTestServer 引入的 Go 版本
    京ICP备2026067502号-1京公网安备11010502063487号