模块 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)
}
})
}
}三种成分:
- 匿名结构体的切片,包含常用字段:
name、输入、预期输出(可能还有预期错误)。 for _, tc := range cases— 执行循环。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) { ... })
}