4. HTTP 接口测试:net/http/httptest 包、创建测试 Server、发送请求、断言响应
在 Go 项目中进行 HTTP 接口测试,核心目标是验证 handler 在真实 HTTP 协议下的行为:路由分发、请求解析、状态码、响应头、响应体以及中间件逻辑。标准库 net/http/httptest 提供了无需外部监听端口的轻量级测试 Server,让集成测试既快速又可靠。
4.1 核心组件:httptest.Server 与 httptest.ResponseRecorder
| 组件 | 用途 | 典型场景 |
|---|---|---|
| httptest.Server | 启动一个真实的 HTTP 测试服务器,分配随机端口 | 需要完整 HTTP 生命周期(连接、TLS、超时)的集成测试 |
| httptest.ResponseRecorder | 记录 handler 写入的响应状态码、Header 和 Body | 单元测试 handler 逻辑,无需启动网络监听 |
| httptest.NewRequest | 构造一个 *http.Request,可设置路径、Header、Body | 配合 ResponseRecorder 模拟客户端请求 |
选择建议: 压测闭环方案中,优先使用
httptest.Server 进行集成测试,因为它能覆盖网络栈、中间件链和路由器的完整行为;ResponseRecorder 更适合 handler 级别的快速验证。
4.2 创建测试 Server:两种模式
模式一:基于现有 handler 创建
func TestUserAPI_WithServer(t *testing.T) {
// 假设已有路由 mux
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/users", handleGetUsers)
// 创建测试 Server
ts := httptest.NewServer(mux)
defer ts.Close()
// 使用 ts.URL 发送请求
resp, err := http.Get(ts.URL + "/api/v1/users")
// ... 断言
}
模式二:自定义 handler 闭包
func TestHealthCheck(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
ts := httptest.NewServer(handler)
defer ts.Close()
// 发送请求
resp, _ := http.Get(ts.URL + "/health")
// ... 断言
}
注意: 每个测试用例都应调用
defer ts.Close(),避免端口泄漏。测试 Server 会在随机端口上监听,不会与开发环境冲突。
4.3 发送请求:构造与执行
使用标准 net/http 客户端发送请求,支持 GET、POST、PUT、DELETE 等所有方法。推荐显式构造 *http.Request 以精确控制 Header 和 Body。
func TestCreateUser(t *testing.T) {
ts := httptest.NewServer(userRouter())
defer ts.Close()
// 构造请求体
body := `{"name":"Alice","email":"alice@example.com"}`
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/users",
strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer test-token-123")
// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, _ := io.ReadAll(resp.Body)
// ... 断言
}
4.4 断言响应:状态码、Header、Body
断言是测试的最终验证环节,必须覆盖三个维度:
- 状态码: 使用
if got != want或 testifyassert.Equal - 响应头: 检查 Content-Type、Location、Set-Cookie 等关键 Header
- 响应体: 对 JSON 响应进行反序列化后断言字段值
func TestGetUser_Assertions(t *testing.T) {
ts := httptest.NewServer(userRouter())
defer ts.Close()
resp, _ := http.Get(ts.URL + "/api/v1/users/42")
defer resp.Body.Close()
// 1. 断言状态码
if resp.StatusCode != http.StatusOK {
t.Errorf("期望状态码 200,得到 %d", resp.StatusCode)
}
// 2. 断言响应头
if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("期望 Content-Type application/json,得到 %s", ct)
}
// 3. 断言响应体
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
t.Fatalf("JSON 解析失败: %v", err)
}
if user.ID != 42 {
t.Errorf("期望 ID 42,得到 %d", user.ID)
}
if user.Name == "" {
t.Error("用户名称不应为空")
}
}
4.5 集成测试流程:从 Server 到断言
4.6 常见断言模式与避坑
| 场景 | 推荐做法 | 常见错误 |
|---|---|---|
| JSON 响应断言 | 使用 json.Decoder 反序列化到结构体,逐字段比较 |
直接比较字符串,忽略字段顺序和空格差异 |
| 错误状态码 | 断言 resp.StatusCode 与 http.StatusXxx 常量相等 |
硬编码数字 400/500,可读性差 |
| 响应头验证 | 使用 resp.Header.Get() 获取单个值 |
忽略 Header 大小写不敏感特性 |
| 空 Body 处理 | 先检查 resp.Body 不为 nil,再读取 |
直接读取 nil Body 导致 panic |
| 超时控制 | 创建带超时的 http.Client,如 &http.Client{Timeout: 5 * time.Second} |
使用默认客户端,测试可能因死锁而挂起 |
4.7 完整示例:用户注册接口集成测试
func TestRegisterUser_Integration(t *testing.T) {
// 1. 创建测试 Server(使用真实路由)
router := setupRouter() // 包含中间件、数据库连接等
ts := httptest.NewServer(router)
defer ts.Close()
// 2. 构造注册请求
registerBody := `{"username":"testuser","password":"Str0ng!Pass"}`
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/auth/register",
strings.NewReader(registerBody))
req.Header.Set("Content-Type", "application/json")
// 3. 发送请求
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("注册请求失败: %v", err)
}
defer resp.Body.Close()
// 4. 断言状态码
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("期望 201 Created,得到 %d,响应体: %s", resp.StatusCode, string(body))
}
// 5. 断言响应头
if loc := resp.Header.Get("Location"); loc == "" {
t.Error("期望 Location 头指向新用户资源")
}
// 6. 断言响应体
var created UserResponse
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
t.Fatalf("JSON 解析失败: %v", err)
}
if created.Username != "testuser" {
t.Errorf("期望用户名 testuser,得到 %s", created.Username)
}
if created.ID == 0 {
t.Error("用户 ID 不应为 0")
}
}
压测闭环要点: 集成测试中的每个断言都对应一个压测指标。例如,状态码断言对应压测中的错误率监控;响应体字段断言对应数据完整性校验。将集成测试用例作为压测场景的“黄金样本”,确保压测流量覆盖所有关键接口路径。