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。
httptest 包三大核心组件关系图 httptest 包三大核心组件关系 httptest.NewServer 启动真实 HTTP 服务器 httptest.NewRecorder 记录 Handler 响应 httptest.NewRequest 构造模拟 HTTP 请求 使用原则 单元测试 → NewRecorder + NewRequest(零网络开销) 集成测试 → 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)
中间件测试流程 中间件测试流程 1. 构造请求 httptest.NewRequest 2. 创建桩 Handler 记录调用信息 3. 包裹中间件 Middleware(stub) 执行 验证维度 ✅ 桩 Handler 是否被正确调用(方法、路径、Header) ✅ 中间件是否修改了响应(状态码、Header、Body) ✅ 异常场景(panic、超时、无效输入)是否被正确处理

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
忽略中间件顺序 中间件链的顺序影响测试结果 在测试中显式构建中间件链,验证整体行为