跳转到主要内容
模块 9 · 第 2 课(共 5)课程中的42/50~14 min
模块课程(2/5)

表驱动测试

表驱动测试是 Go 的惯用模式:您无需编写 N 个单独的 TestThisSpecificCase 函数,而是声明一组 case 并进行迭代。有一个断言块,测试数据与逻辑分离。

基础图案

Go
func TestAbs(t *testing.T) {
    cases := []struct {
        name string
        in   int
        want int
    }{
        {"pos", 3, 3},
        {"neg", -3, 3},
        {"zero", 0, 0},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            if got := Abs(tc.in); got != tc.want {
                t.Errorf("Abs(%d) = %d; voglio %d", tc.in, got, tc.want)
            }
        })
    }
}

三种成分:

  1. 匿名结构体的切片,包含常用字段:name、输入、预期输出(可能还有预期错误)。
  2. for _, tc := range cases — 执行循环。
  3. t.Run(tc.name, func(t *testing.T) { ... }) — 每个案例都成为一个具有自己名称的子测试。

为什么t.Run?

t.Run 创建一个子测试,其中包含:

  • 输出中显示的 个人名称:TestAbs/pos、TestAbs/neg、...
  • 它自己的 t *testing.T:一种情况下的失败不会阻止其他情况(除非使用 t.Fatal )。
  • 命令行过滤:go test -run TestAbs/neg 仅在这种情况下运行。
  • 可选并行性:在 func 内调用 t.Parallel() 进行并行化。
Bash
go test -v -run TestAbs/neg

存在预期错误的情况

Go
cases := []struct {
    name    string
    in      string
    want    int
    wantErr bool
}{
    {"ok", "42", 42, false},
    {"bad", "x", 0, true},
}
for _, tc := range cases {
    t.Run(tc.name, func(t *testing.T) {
        got, err := Atoi(tc.in)
        if (err != nil) != tc.wantErr {
            t.Fatalf("err = %v, wantErr = %v", err, tc.wantErr)
        }
        if !tc.wantErr && got != tc.want {
            t.Errorf("got %d, want %d", got, tc.want)
        }
    })
}

循环闭合陷阱(Go 1.22 之前的版本)

在 Go 1.22 之前的版本中,tc 变量在迭代之间共享:在 t.Run 内调用 t.Parallel() 会导致所有子测试针对相同的最后一个案例运行。修复方法是:

Go
for _, tc := range cases {
    tc := tc // local rebind (pre-1.22)
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        // ...
    })
}

命名案例

最佳实践:

  • 简短但令人回味的名称:"empty input"、"too large"、"negative"。
  • 如果您想使用 -run 轻松过滤(或使用下划线),则不能有空格。
  • 始终包含一种“幸福路径”情况、一种“边缘”情况(零、空)和至少一种错误情况。

练习

锻炼#go.m9.l2.e1
尝试:0加载中...

定义匿名结构的案例切片,其中包含字段 in 和 Want,并填充 3 个案例来测试 Abs(正、负、零)。

正在加载编辑器...

3 次尝试后可用的解决方案

锻炼#go.m9.l2.e2
尝试:0加载中...

添加 t.Run 和 tc.name 将每个案例转换为命名子测试。

正在加载编辑器...

3 次尝试后可用的解决方案

Quiz#go.m9.l2.e3
准备好

与 N 个单独的测试函数相比,表驱动模式的主要优点是什么?

Go
for _, tc := range cases {
  t.Run(tc.name, func(t *testing.T) { ... })
}
答案选项