httptest.NewServer 启动真实HTTP服务器用于客户端集成测试,需调用 Close();NewRecorder 用于 handler 单元测试,需手动检查 Code、Header 和 Body。

用 httptest.NewServer 启动真实可调用的测试服务
当你需要验证客户端代码(比如 http.Client)是否能正确请求、处理响应时,httptest.NewServer 比 httptest.NewRecorder 更贴近真实场景。它会启动一个监听本地端口的真实 HTTP 服务器,返回可用的 URL,客户端可直接发起请求。
常见错误是误以为 NewRecorder 能模拟服务端对外暴露的地址——它只记录请求/响应,不监听端口,无法被外部访问。
- 适合测试带重试、超时、跳转、证书校验等行为的客户端逻辑
- 启动后必须调用
server.Close(),否则测试进程可能卡住或端口复用失败 - 返回的
server.URL是完整地址(如"http://127.0.0.1:34212"),可直接传给http.Get或自定义http.Client
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/users" && r.Method == "GET" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`[{"id":1,"name":"alice"}]`))
}
}))
defer server.Close() // 必须加
resp, err := http.Get(server.URL + "/api/v1/users")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
用 httptest.NewRecorder 测试 handler 函数本身
如果你要单元测试某个 http.HandlerFunc 或 Gin/Echo 的路由处理函数,不需要网络开销,就该用 httptest.NewRecorder。它实现了 http.ResponseWriter 接口,把响应内容缓存在内存里,供断言检查。
容易忽略的是:它不会自动设置默认状态码。如果 handler 没显式调用 w.WriteHeader,recorder.Code 默认为 0,不是 200。
立即学习“go语言免费学习笔记(深入)”;
- 适用于快速验证路由逻辑、中间件行为、JSON 序列化、Header 设置等
- 注意检查
recorder.Code、recorder.Header()和recorder.Body.String() - 对 POST/PUT 请求,需手动构造
*http.Request并设置Body和Content-Type
req := httptest.NewRequest("POST", "/login", strings.NewReader(`{"user":"bob","pass":"123"}`))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(loginHandler)
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status OK, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "token") {
t.Error("response body doesn't contain token")
}
测试带依赖的 handler:用接口隔离数据库或外部服务
真实 handler 往往依赖数据库、缓存、第三方 API。硬编码调用会导致测试慢、不稳定、难 mock。Golang 的惯用做法是把依赖抽象为接口,并在测试时注入 mock 实现。
例如 handler 依赖一个 UserRepository 接口,测试时传入一个只实现必要方法的匿名结构体,而非启动真实 DB。
- 避免在测试中使用
os.Setenv或全局变量切换环境——易污染、难并行 - mock 实现应只覆盖测试路径所需方法,其余方法可 panic 或返回零值(明确暴露未覆盖路径)
- 若 handler 使用了
context.Context(如带 timeout 或 trace ID),测试时建议传入context.Background()或带取消的测试 context
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
}
func TestGetUserHandler(t *testing.T) {
mockRepo := &mockUserRepo{user: &User{ID: 123, Name: "carol"}}
handler := makeGetUserHandler(mockRepo)
req := httptest.NewRequest("GET", "/users/123", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// 断言响应
}
验证 JSON 响应结构:别只用 strings.Contains
用字符串匹配检查 JSON 响应既脆弱又难维护。字段顺序变化、空格增减、嵌套结构变动都会让测试意外失败。应该反序列化后再断言字段值或结构。
但要注意:如果 handler 返回非标准 JSON(比如带注释、多空格、换行缩进不一致),json.Unmarshal 仍能成功;而严格格式校验(如用 json.RawMessage 或第三方库)通常没必要。
- 优先用
json.Unmarshal解析到 struct 或map[string]interface{},再检查关键字段 - 对错误响应,也要验证
Code和 error 字段(如"error": "not found") - 避免对整个 JSON 字符串做
==比较——浮点数精度、时间格式、字段顺序都可能导致误判
var data []map[string]interface{}
if err := json.Unmarshal(rr.Body.Bytes(), &data); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(data) == 0 || data[0]["id"] != float64(1) {
t.Error("expected user with id=1")
}
测试 HTTP handler 的核心在于分清「测什么」:测 handler 逻辑本身,用 NewRecorder;测客户端集成行为,用 NewServer;所有外部依赖必须可替换,否则测试就不是单元测试。最容易被跳过的其实是清理步骤(server.Close()、db.Close())和状态码显式设置——它们不出错时不报,一出错就难定位。










