bigywater.tech
Gin · 53

测试:httptest 与 CreateTestContext

给 Gin 写测试,是用 httptest.NewRecorder 还是 gin.CreateTestContext?它们各自缺什么、会让哪些 bug 逃过测试、哪些情况必须起真实服务器?这篇用一个小应用跑真实的 go test,把它们的行为和耗时逐项对出来。

2.1 vs 53.1 µs
一次请求:ServeHTTP + 记录器 与真实回环 HTTP 的耗时(约 25 倍)
c.Param 为空
CreateTestContext 的 Context 没有 Params,getUser 输出 id="",手工设置 c.Params 后才是 "7"
X-Late
记录器 w.Header() 能看到写完才设的头;w.Result().Header 与真实客户端看不到

NewRecorder + ServeHTTP

实测 · 本机执行输出
  [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
  [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  [GIN-debug] GET    /users/:id                --> lab.setup.func2 (4 handlers)
  [GIN-debug] POST   /echo                     --> lab.setup.func3 (4 handlers)
  [GIN-debug] GET    /late                     --> lab.setup.func4 (4 handlers)
  main_test.go:9: NewRecorder:状态 200,Content-Type="application/json; charset=utf-8",X-MW="1",body=map[full:/users/:id id:42 ip:192.0.2.1 q:x]
  main_test.go:10: httptest.NewRequest 的默认值:Host="example.com" RemoteAddr="192.0.2.1:1234" Proto="HTTP/1.1"
  main_test.go:13: POST 没设 Content-Type:ShouldBindJSON 仍按 JSON 解码 → 201 {"a":1}
  main_test.go:16: 坏 JSON → 400 {"error":"invalid character 'b' looking for beginning of object key string"}
  ok  	lab	0.363s

这是测路由、中间件、绑定、响应的首选:引擎真实地走完整条链,只是不经过网络。注意 httptest.NewRequest 的默认 RemoteAddr 是 192.0.2.1:1234,所以依赖 ClientIP 的逻辑(限流、白名单)在测试里看到的总是这个地址,需要时手工设置 req.RemoteAddr。

CreateTestContext

实测 · 本机执行输出
  [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  main_test.go:21: CreateTestContext:c.Request==nil true;c.Params=[];c.FullPath()="";eng 路由数 0
  main_test.go:23: 没设 Request 就调 getUser(c) → panic: runtime error: invalid memory address or nil pointer dereference
  main_test.go:28: 设了 Request 但 c.Param("id") 为空 → {"id":"","path":"/users/7"}
  [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  main_test.go:32: 手工设置 c.Params 之后 → {"id":"7","path":"/users/7"}(状态 200)
  [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  main_test.go:36: c.Set / c.Get 可用:v;c.Next() 无 handler,c.IsAborted()=false;c.Writer.Status()=200
  ok  	lab	0.474s

得到的是一个空的 Context:没有 Request、没有路径参数、没有路由。适合直接调用单个 handler 函数(先设好 Request,必要时手工设 c.Params),不适合测路由匹配和中间件顺序——那些要用上面的方式。

记录器的盲区

实测 · 本机执行输出
  [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
  [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  [GIN-debug] GET    /users/:id                --> lab.setup.func2 (4 handlers)
  [GIN-debug] POST   /echo                     --> lab.setup.func3 (4 handlers)
  [GIN-debug] GET    /late                     --> lab.setup.func4 (4 handlers)
  main_test.go:41: 写完 body 之后才设的头:w.Header()(活的 map)X-Late="1";w.Result().Header(响应快照)X-Late=""
  main_test.go:44: 真实服务器 + 真实客户端:X-Late=""(这才是客户端会看到的)
  ok  	lab	0.469s

ResponseRecorder.Header() 返回的是活的 map,写出之后再改的头也能看到;真实服务器里这些头根本发不出去。想断言“客户端会看到什么”,用 w.Result().Header,或者起 httptest.NewServer。超时、客户端断开、HTTP/2、真实的 Hijack 也只有真实服务器才测得出来。

测试模式

实测 · 本机执行输出
  main_test.go:46: go test 里 gin.Mode()="debug"(setup 里的 gin.Default() 在 debug 下会打印上面的 [GIN-debug] 行)
  ok  	lab	0.476s
  加了 TestMain + SetMode(TestMode) 之后,输出共 5 行:
  | === RUN   TestMode
  | main_test.go:47: go test 里 gin.Mode()="test"(setup 里的 gin.Default() 在 debug 下会打印上面的 [GIN-debug] 行)
  | --- PASS: TestMode (0.00s)
  | PASS
  | ok  	lab	0.463s

go test 里不设置 GIN_MODE 时 Gin 是 debug 模式,每次创建引擎都会输出警告和路由表。在 TestMain 里 gin.SetMode(gin.TestMode) 就安静了。

速度

实测 · 本机执行输出
    NewRecorder + NewRequest + ServeHTTP:2.1 µs/次
    真实 HTTP(httptest.NewServer + http.Client,keep-alive):53.1 µs/次(约 25 倍)

进程内的 ServeHTTP 只要几微秒,几千个用例也是毫秒级;真实 HTTP 约慢 25 倍,但仍然是几十微秒,所以是否起服务器应该由“要不要测网络行为”决定,而不是性能。

分步演示

测试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。第一到四节是真实的 go test -v(每个实验单独用 -run 运行,Go 1.27.1,测试包名为 lab);第五节用 testing.Benchmark 的 5 轮中位数;第三节的真实服务器是 httptest.NewServer,客户端是 http.Get;每种情形各一次
    • 没有验证:并行测试(t.Parallel)下共享引擎的并发安全(ServeHTTP 本身可并发,没有用 -race 测);表驱动测试的写法(只是风格);gin.CreateTestContextOnly 等其它辅助函数(源码里有,没有试);Mock 数据库等依赖的方式;测试覆盖率工具
    京ICP备2026067502号-1京公网安备11010502063487号