单元测试的表驱动写法与 httptest 怎么用?
一句话结论:表驱动测试把“输入-期望输出”整理成切片,用 t.Run 起子测试逐行执行,失败时能精确定位到用例名;httptest.NewRecorder 直接测 handler 的响应,httptest.NewServer 起真实 HTTP 服务测客户端代码。测试文件放 *_test.go,配合 -race 与 -cover 是工程标配。
表驱动基本骨架
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"正数", 1, 2, 3},
{"含零", 0, 5, 5},
{"负数", -1, -1, -2},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := add(tc.a, tc.b); got != tc.want {
t.Fatalf("add(%d,%d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
})
}
}
- 断言失败用 t.Errorf/t.Fatalf;Fatal 终止当前 goroutine,在子测试内使用安全。
- 子测试名不要含空格与斜杠;可用嵌套 t.Run 组织层次。
- 子测试里开 t.Parallel() 时注意用例共享变量(Go 1.22 前要 tc := tc 复制)。
httptest 两件套
func TestHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/user/1", nil)
rec := httptest.NewRecorder() // 记录 ResponseWriter
userHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("code = %d, want 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), "alice") {
t.Fatalf("body = %s", rec.Body.String())
}
}
func TestClient(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("pong"))
}))
defer srv.Close()
// 把 srv.URL 交给被测的 http.Client 逻辑
}
工程化习惯
- 测试不依赖真实端口、真实时间与外部服务;外部依赖用接口替身或 httptest。
- go test -race ./... 排查竞争;go test -cover 看覆盖;go vet ./... 例行检查。
- 表驱动加子测试让“哪个用例挂了”一目了然,别堆一大堆几乎重复的 TestXxx。
常见追问 / 记忆点
- 追问:测 handler 用 NewRecorder 与 NewServer 有什么区别?——Recorder 快且免端口,测单个 handler;NewServer 起真服务,测重定向与客户端行为。
- 追问:需要 mock 依赖时怎么做?——把依赖抽象成小接口注入,测试传假实现,这也是依赖注入的主要动机之一。
- 记忆点:表驱动管逻辑、httptest 管 HTTP;测试里没有真实端口与外部依赖。