跳转到内容

测试 Handler

本指南展示如何使用标准库的 net/http/httptest 包测试 Plumego handler。由于每个 Plumego handler 都是普通的 http.HandlerFunc,不需要任何框架专属的测试助手。

  • 测试读取 query param 的 handler
  • 测试使用注入依赖的 handler
  • 验证 contract.WriteErrorcontract.WriteResponse 的输出
  • 检查响应体和状态码

第一步 — 测试读取 query param 的 handler

Section titled “第一步 — 测试读取 query param 的 handler”

参考服务中的 Greet handler:

// 被测 handler
func (h APIHandler) Greet(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeRequired).
Detail("field", "name").
Message("name is required").
Build())
return
}
_ = contract.WriteResponse(w, r, http.StatusOK,
greetResponse{Message: "hello, " + name}, nil)
}

测试代码:

package handler_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"myapp/internal/handler"
)
func TestGreet_MissingName(t *testing.T) {
h := handler.APIHandler{}
r := httptest.NewRequest(http.MethodGet, "/api/v1/greet", nil)
w := httptest.NewRecorder()
h.Greet(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", w.Code)
}
}
func TestGreet_WithName(t *testing.T) {
h := handler.APIHandler{}
r := httptest.NewRequest(http.MethodGet, "/api/v1/greet?name=Alice", nil)
w := httptest.NewRecorder()
h.Greet(w, r)
if w.Code != http.StatusOK {
t.Fatalf("want 200, got %d", w.Code)
}
var body struct {
Data struct{ Message string } `json:"data"`
}
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Data.Message != "hello, Alice" {
t.Errorf("want 'hello, Alice', got %q", body.Data.Message)
}
}

第二步 — 测试带注入依赖的 handler

Section titled “第二步 — 测试带注入依赖的 handler”

对于持有依赖(DB、logger、外部客户端)的 handler,通过 handler 结构体传入测试替身:

type fakeRepo struct {
item *Item
err error
}
func (f *fakeRepo) Find(_ context.Context, _ string) (*Item, error) {
return f.item, f.err
}
func TestItemHandler_NotFound(t *testing.T) {
h := &handler.ItemHandler{
Repo: &fakeRepo{err: repo.ErrNotFound},
}
r := httptest.NewRequest(http.MethodGet, "/api/items?id=999", nil)
w := httptest.NewRecorder()
h.Get(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("want 404, got %d", w.Code)
}
}

contract.WriteResponse 将数据包装在 {"data": ...} 中。contract.WriteError 使用 {"error": ...}。在断言中注意这一点:

// 成功响应
var success struct {
Data json.RawMessage `json:"data"`
}
json.NewDecoder(w.Body).Decode(&success)
// 错误响应
var errResp struct {
Error struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error"`
}
json.NewDecoder(w.Body).Decode(&errResp)

第四步 — 针对多场景使用表驱动测试

Section titled “第四步 — 针对多场景使用表驱动测试”

对于有多种输入情况的 handler,表驱动测试优于重复的独立测试函数:

func TestGreet(t *testing.T) {
h := handler.APIHandler{}
tests := []struct {
name string
query string
status int
body string
}{
{"缺少 name", "/api/v1/greet", http.StatusBadRequest, ""},
{"alice", "/api/v1/greet?name=Alice", http.StatusOK, "hello, Alice"},
{"bob", "/api/v1/greet?name=Bob", http.StatusOK, "hello, Bob"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, tc.query, nil)
w := httptest.NewRecorder()
h.Greet(w, r)
if w.Code != tc.status {
t.Fatalf("期望状态 %d,实际 %d", tc.status, w.Code)
}
if tc.body != "" && !strings.Contains(w.Body.String(), tc.body) {
t.Errorf("期望响应体包含 %q,实际 %s", tc.body, w.Body.String())
}
})
}
}

Middleware 接受并返回 http.Handler,因此也可以直接测试:

func TestRecovery_Panic(t *testing.T) {
logger := log.NewLogger()
mw, err := recovery.Middleware(recovery.Config{Logger: logger})
if err != nil {
t.Fatalf("configure recovery middleware: %v", err)
}
panicHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("deliberate panic")
})
r := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
mw(panicHandler).ServeHTTP(w, r)
if w.Code != http.StatusInternalServerError {
t.Fatalf("want 500, got %d", w.Code)
}
}
现象先检查
状态码不符合预期在 handler 运行后断言 w.Code,不要提前断言
响应体断言失败contract envelope 解析:成功响应在 data 下,错误响应在 error
handler 需要路径参数或 request ID调用 handler 前,按 router 或 middleware 的方式构造 request context
依赖行为难以控制通过 handler 字段或 interface 传入测试替身
middleware 测试没有覆盖最终 handler调用 mw(next).ServeHTTP(w, r),并让 next 记录自己被执行
  • 不需要测试框架或 HTTP server — httptest.NewRecorder 捕获完整响应。
  • 带有接口字段的 handler 结构体无需反射或 mock 库即可接受任何测试替身。
  • 测试运行快速:没有监听 socket、没有网络往返、没有 OS 资源消耗。
  • 同一个 http.HandlerFunc 形状无需适配即可用于生产、测试和基准测试。

reference/standard-service/internal/handler/api.go 展示了带注入依赖的 handler 结构体,其形状与本指南描述一致。