单元测试的表驱动写法与 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;测试里没有真实端口与外部依赖。
笔记加载中…