给 Gin 写测试,是用 httptest.NewRecorder 还是 gin.CreateTestContext?它们各自缺什么、会让哪些 bug 逃过测试、哪些情况必须起真实服务器?这篇用一个小应用跑真实的 go test,把它们的行为和耗时逐项对出来。
[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。
[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 倍,但仍然是几十微秒,所以是否起服务器应该由“要不要测网络行为”决定,而不是性能。