如何在Golang中测试HTTP接口_Golang httptest模拟与接口验证方法

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

httptest.NewServer 启动真实可调用的测试服务

当你需要验证客户端代码(比如 http.Client)是否能正确请求、处理响应时,httptest.NewServerhttptest.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.WriteHeaderrecorder.Code 默认为 0,不是 200。

  • 适用于快速验证路由逻辑、中间件行为、JSON 序列化、Header 设置等
  • 注意检查 recorder.Coderecorder.Header()recorder.Body.String()
  • 对 POST/PUT 请求,需手动构造 *http.Request 并设置 BodyContent-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())和状态码显式设置——它们不出错时不报,一出错就难定位。