3、HTTP服务测试基础:net/http/httptest包介绍、创建测试Server、发送测试请求、断言响应
在Go语言生态中,net/http/httptest 是标准库提供的HTTP测试利器。它允许开发者在不启动真实端口监听的情况下,创建内存中的HTTP测试服务器,并直接发送请求、验证响应。本章将系统讲解 httptest 的核心组件、Server 创建、请求发送与响应断言,为后续契约压测打下坚实基础。
3.1 httptest 包核心组件
httptest 包主要包含三个核心类型,它们协同工作,覆盖了HTTP测试的完整链路:
| 组件 | 类型 | 职责 | 关键方法/字段 |
|---|---|---|---|
| Server | *httptest.Server |
在内存中启动一个HTTP服务,自动分配可用端口 | NewServer(), Close(), URL, Config |
| ResponseRecorder | *httptest.ResponseRecorder |
记录HTTP处理器的响应状态码、Header、Body | NewRecorder(), Code, HeaderMap, Body |
| Request | *http.Request |
构造测试用的HTTP请求(标准 net/http 类型) |
http.NewRequest(), httptest.NewRequest() |
关键区别:
httptest.Server 启动一个真实的HTTP监听器(但绑定到随机端口,不暴露到外部网络),适合集成测试;httptest.ResponseRecorder 直接调用处理器函数,不经过网络栈,适合单元测试。两者在契约压测中各有应用场景。
3.2 创建测试 Server
创建测试Server有两种主要方式:基于 http.Handler 和基于 http.ServeMux。以下展示最常用的 httptest.NewServer 方法:
// 定义一个简单的处理函数
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "Hello, 契约压测!"}`))
}
// 创建测试 Server
func createTestServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/api/hello", helloHandler)
return httptest.NewServer(mux)
}
// 使用示例
func TestHelloEndpoint(t *testing.T) {
ts := createTestServer()
defer ts.Close()
// ts.URL 格式为 http://127.0.0.1:xxxxx
t.Logf("测试服务器地址: %s", ts.URL)
}
重要实践: 务必在测试函数中调用
defer ts.Close(),确保每个测试用例结束后释放资源。测试Server默认使用 http.DefaultServeMux,但推荐显式创建 http.ServeMux 以隔离路由。
以下SVG展示了测试Server的创建与生命周期:
3.3 发送测试请求
httptest 支持两种请求发送方式,分别对应不同的测试粒度:
- 直接调用 Handler(单元测试风格):使用
httptest.NewRecorder()和http.Handler.ServeHTTP(),不经过网络栈,速度最快。 - 通过 Server URL 发送(集成测试风格):使用
http.Get()、http.Post()或http.Client.Do(),经过完整的HTTP序列化/反序列化,更贴近真实场景。
// 方式一:直接调用 Handler(推荐用于单元测试)
func TestDirectHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/hello", nil)
w := httptest.NewRecorder()
helloHandler(w, req)
resp := w.Result()
// 后续断言...
}
// 方式二:通过 Server URL 发送(推荐用于契约压测)
func TestViaServer(t *testing.T) {
ts := createTestServer()
defer ts.Close()
// 使用标准 http.Client 发送请求
resp, err := http.Get(ts.URL + "/api/hello")
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
// 后续断言...
}
性能对比: 方式一(直接调用)单次请求耗时约 0.1~0.5μs,方式二(通过Server)约 10~50μs。在契约压测中,若需模拟真实网络开销(如序列化、连接池),应使用方式二;若仅验证逻辑正确性,方式一更高效。
3.4 断言响应
响应断言是测试的核心环节,通常需要验证三个维度:状态码、响应头、响应体。以下是一个完整的断言示例:
func TestHelloResponse(t *testing.T) {
ts := createTestServer()
defer ts.Close()
resp, err := http.Get(ts.URL + "/api/hello")
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
// 1. 断言状态码
if resp.StatusCode != http.StatusOK {
t.Errorf("期望状态码 %d,实际 %d", http.StatusOK, resp.StatusCode)
}
// 2. 断言响应头
contentType := resp.Header.Get("Content-Type")
if contentType != "application/json" {
t.Errorf("期望 Content-Type 为 application/json,实际 %s", contentType)
}
// 3. 断言响应体
body, _ := io.ReadAll(resp.Body)
expected := `{"message": "Hello, 契约压测!"}`
if string(body) != expected {
t.Errorf("响应体不匹配\n期望: %s\n实际: %s", expected, string(body))
}
}
以下SVG展示了响应断言的完整流程与关键检查点:
3.5 常见断言模式与最佳实践
在实际的契约压测中,响应断言需要兼顾精确性与可维护性。以下是三种常见断言模式:
| 模式 | 适用场景 | 示例 | 注意事项 |
|---|---|---|---|
| 精确匹配 | 响应体固定不变的接口 | string(body) == expectedJSON |
对字段顺序敏感,适合静态契约 |
| 结构匹配 | 响应体包含动态字段(如时间戳) | json.Unmarshal 后断言特定字段 |
忽略动态值,只验证契约字段 |
| 正则匹配 | 响应体包含模式化内容(如UUID) | regexp.MatchString(pattern, body) |
避免过度使用,影响测试可读性 |
// 结构匹配示例:只验证关键字段
func TestPartialMatch(t *testing.T) {
ts := createTestServer()
defer ts.Close()
resp, _ := http.Get(ts.URL + "/api/hello")
defer resp.Body.Close()
var result struct {
Message string `json:"message"`
}
json.NewDecoder(resp.Body).Decode(&result)
if result.Message != "Hello, 契约压测!" {
t.Errorf("Message 字段不匹配,期望 'Hello, 契约压测!',实际 '%s'", result.Message)
}
}
契约压测建议: 在压测场景中,推荐使用 结构匹配 模式。精确匹配容易因微小格式差异(如空格、换行)导致误判;正则匹配在压测循环中性能开销较大。结构匹配既能保证契约字段正确,又能容忍合理的动态变化。
3.6 本章小结
通过本章学习,你已掌握 httptest 包的三大核心组件(Server、ResponseRecorder、Request),能够独立创建测试Server、发送请求并完成三维度断言。这些基础能力将直接应用于后续的契约定义、参数配置与压测执行环节。在下一阶段,我们将基于这些基础,构建可复用的契约测试框架。