4. httptest 包:HTTP 服务端测试、HTTP 客户端测试、Mock HTTP 请求、测试中间件
在微服务与 API 驱动的架构中,HTTP 层是系统间通信的核心枢纽。Go 标准库 net/http/httptest 包为 HTTP 测试提供了“零依赖、高保真”的官方方案,无需启动真实端口即可完成服务端、客户端、中间件的全链路验证。本章将围绕四个核心场景展开:服务端测试、客户端测试、Mock 外部请求、中间件测试。
4.1 核心组件速览
httptest 包主要提供三个构造块,理解其职责是后续实战的基础。
| 组件 | 类型 | 核心用途 | 典型场景 |
|---|---|---|---|
| httptest.NewServer | 函数 | 基于真实 http.Handler 启动一个测试用 HTTP 服务器,自动分配可用端口 |
集成测试、客户端真实请求验证 |
| httptest.NewRecorder | 结构体 | 实现 http.ResponseWriter,记录 Handler 写入的状态码、Header 和 Body |
单元测试、Handler 行为验证 |
| httptest.NewRequest | 函数 | 构造一个 *http.Request,支持设置方法、路径、Body 和上下文 |
模拟请求、中间件测试 |
选择建议: 单元测试优先使用
NewRecorder + NewRequest,无需网络开销;集成测试或需要验证客户端重定向、Cookie 传播时,使用 NewServer。
4.2 HTTP 服务端测试
服务端测试的核心是验证 Handler 对输入请求的处理逻辑是否正确。使用 httptest.NewRecorder 和 httptest.NewRequest 可以完全避免网络层干扰。
4.2.1 基础 Handler 测试
func TestGreetingHandler(t *testing.T) {
// 1. 构造请求
req := httptest.NewRequest(http.MethodGet, "/greet?name=Alice", nil)
// 2. 创建响应记录器
rec := httptest.NewRecorder()
// 3. 调用目标 Handler
GreetingHandler(rec, req)
// 4. 验证响应
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Hello, Alice")
}
4.2.2 请求 Body 与 Header 验证
func TestCreateUserHandler(t *testing.T) {
body := `{"name":"Bob","email":"bob@example.com"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
CreateUserHandler(rec, req)
assert.Equal(t, http.StatusCreated, rec.Code)
assert.JSONEq(t, `{"id":1,"name":"Bob"}`, rec.Body.String())
}
关键检查点:
- 状态码:
rec.Code验证 HTTP 语义是否正确。 - 响应头:
rec.Header()验证 Content-Type、Location 等。 - 响应体:
rec.Body.String()验证 JSON/XML/文本内容。 - Flush 行为:若 Handler 使用
http.Flusher,需调用rec.Flush()后检查。
4.3 HTTP 客户端测试
当被测代码包含 HTTP 客户端调用(如调用外部 API)时,httptest.NewServer 可以模拟远端服务,让测试不依赖真实网络。
4.3.1 使用 NewServer 模拟远端
func TestFetchUser(t *testing.T) {
// 1. 启动测试服务器
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/user/1", r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id":1,"name":"Mock User"}`))
}))
defer mockServer.Close()
// 2. 将客户端 BaseURL 指向 mockServer.URL
client := NewUserClient(mockServer.URL)
user, err := client.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Mock User", user.Name)
}
4.3.2 验证客户端行为
| 验证维度 | 测试方法 | 示例断言 |
|---|---|---|
| 请求方法 | 在 mock Handler 中检查 r.Method |
assert.Equal(t, "POST", r.Method) |
| 请求路径 | 检查 r.URL.Path 和 r.URL.RawQuery |
assert.Equal(t, "/v2/orders", r.URL.Path) |
| 请求 Header | 检查 r.Header |
assert.Equal(t, "Bearer token123", r.Header.Get("Authorization")) |
| 请求 Body | 读取 r.Body 并反序列化 |
assert.JSONEq(t, expectedBody, readBody(r)) |
| 超时/重试 | mock 返回特定状态码或延迟响应 | assert.True(t, errors.Is(err, context.DeadlineExceeded)) |
4.4 Mock HTTP 请求
在服务端测试中,有时需要模拟来自上游的复杂请求(如带签名、Cookie、TLS 证书)。httptest.NewRequest 提供了灵活的构造能力。
4.4.1 模拟带上下文的请求
func TestAuthMiddlewareWithContext(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/admin", nil)
// 注入用户信息到上下文
ctx := context.WithValue(req.Context(), "user_id", 42)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid := r.Context().Value("user_id").(int)
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("User:%d", uid)))
})).ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "User:42")
}
4.4.2 模拟 TLS 与 Cookie
func TestSecureHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/secure", nil)
// 模拟 TLS 连接
req.TLS = &tls.ConnectionState{PeerCertificates: []*x509.Certificate{testCert}}
// 添加 Cookie
req.AddCookie(&http.Cookie{Name: "session", Value: "abc123"})
rec := httptest.NewRecorder()
SecureHandler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
}
注意:
httptest.NewRequest 默认不设置 RequestURI 字段。如果 Handler 依赖 r.RequestURI 而非 r.URL.String(),需要手动赋值:req.RequestURI = req.URL.String()。
4.5 测试中间件
中间件是 HTTP 处理链中的关键环节。测试中间件需要验证其在请求前、请求后以及异常场景下的行为。
4.5.1 中间件测试模式
标准测试模式:构造一个“桩 Handler”,将其包裹在目标中间件中,然后验证桩 Handler 收到的请求上下文或中间件写入的响应。
func TestLoggingMiddleware(t *testing.T) {
var loggedMethod, loggedPath string
// 桩 Handler:记录中间件传递的信息
stub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
loggedMethod = r.Method
loggedPath = r.URL.Path
w.WriteHeader(http.StatusOK)
})
// 包裹中间件
handler := LoggingMiddleware(stub)
req := httptest.NewRequest(http.MethodPost, "/api/data", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, "POST", loggedMethod)
assert.Equal(t, "/api/data", loggedPath)
assert.Equal(t, http.StatusOK, rec.Code)
}
4.5.2 中间件测试清单
| 中间件类型 | 测试重点 | 典型断言 |
|---|---|---|
| 日志中间件 | 是否记录了方法、路径、状态码、耗时 | assert.Contains(t, logOutput, "GET /api") |
| 认证中间件 | 有效 Token 放行,无效 Token 返回 401 | assert.Equal(t, http.StatusUnauthorized, rec.Code) |
| 限流中间件 | 超过阈值返回 429,正常请求放行 | assert.Equal(t, http.StatusTooManyRequests, rec.Code) |
| CORS 中间件 | 预检请求返回正确 Header | assert.Equal(t, "*", rec.Header().Get("Access-Control-Allow-Origin")) |
| 恢复中间件 | Handler panic 后返回 500 而非崩溃 | assert.Equal(t, http.StatusInternalServerError, rec.Code) |
4.6 实战:完整测试套件示例
以下示例展示如何将服务端、客户端、Mock 和中间件测试组合到一个完整的测试套件中。
// handler.go
func HealthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// handler_test.go
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
HealthHandler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, `{"status":"ok"}`, rec.Body.String())
}
func TestAuthMiddleware_ValidToken(t *testing.T) {
stub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
handler := AuthMiddleware(stub)
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
req.Header.Set("Authorization", "Bearer valid-token")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
}
func TestAuthMiddleware_MissingToken(t *testing.T) {
stub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
handler := AuthMiddleware(stub)
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
func TestClientWithMockServer(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/health", r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}))
defer mockServer.Close()
client := NewHealthClient(mockServer.URL)
status, err := client.CheckHealth()
assert.NoError(t, err)
assert.Equal(t, "ok", status)
}
测试金字塔建议:
- 底层:大量使用
NewRecorder + NewRequest测试 Handler 和中间件逻辑(单元测试)。 - 中层:使用
NewServer测试客户端与外部服务的交互(集成测试)。 - 顶层:少量端到端测试验证真实路由和中间件链的协作。
4.7 常见陷阱与最佳实践
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 忘记关闭 Server | httptest.NewServer 会占用端口,不关闭导致资源泄漏 |
使用 defer server.Close() 确保清理 |
| 忽略 RequestURI | 某些路由库依赖 r.RequestURI |
手动设置 req.RequestURI = req.URL.String() |
| Body 未重置 | 多次读取 r.Body 会导致 EOF |
使用 io.NopCloser(bytes.NewReader(...)) 重新构造 |
| 并发测试共享状态 | 多个测试用例共享同一个 Recorder 或 Server | 每个测试用例独立创建 Recorder 和 Server |
| 忽略中间件顺序 | 中间件链的顺序影响测试结果 | 在测试中显式构建中间件链,验证整体行为 |