测试 Handler
测试 Handler
Section titled “测试 Handler”本指南展示如何使用标准库的 net/http/httptest 包测试 Plumego handler。由于每个 Plumego handler 都是普通的 http.HandlerFunc,不需要任何框架专属的测试助手。
- 测试读取 query param 的 handler
- 测试使用注入依赖的 handler
- 验证
contract.WriteError和contract.WriteResponse的输出 - 检查响应体和状态码
第一步 — 测试读取 query param 的 handler
Section titled “第一步 — 测试读取 query param 的 handler”参考服务中的 Greet handler:
// 被测 handlerfunc (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 响应体
Section titled “第三步 — 解析 contract 响应体”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
Section titled “第五步 — 单独测试 middleware”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) }}如果没有按预期工作
Section titled “如果没有按预期工作”| 现象 | 先检查 |
|---|---|
| 状态码不符合预期 | 在 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 记录自己被执行 |
这种模式带来什么
Section titled “这种模式带来什么”- 不需要测试框架或 HTTP server —
httptest.NewRecorder捕获完整响应。 - 带有接口字段的 handler 结构体无需反射或 mock 库即可接受任何测试替身。
- 测试运行快速:没有监听 socket、没有网络往返、没有 OS 资源消耗。
- 同一个
http.HandlerFunc形状无需适配即可用于生产、测试和基准测试。
参考应用中的完整示例
Section titled “参考应用中的完整示例”reference/standard-service/internal/handler/api.go 展示了带注入依赖的 handler 结构体,其形状与本指南描述一致。