表驱动测试是用结构体切片封装测试用例并循环执行的惯用模式,比多个Test函数更简洁易维护;核心是定义含name、input、want、err等字段的struct切片,配合t.Run实现清晰可读的子测试。

什么是表驱动测试:用 slice 装测试用例比写多个 TestXXX 更省事
Go 语言里,表驱动测试(table-driven test)不是语法特性,而是一种组织 Test 函数的惯用模式:把输入、预期输出、描述等封装成结构体切片,再用一个 for 循环跑完全部用例。它避免了为每个场景单独写 func TestXxx(t *testing.T),也比嵌套 if 判断更易读、易维护。
怎么定义测试表:用 struct + slice 是最通用的方式
核心是定义一个匿名或具名结构体,字段覆盖你需要验证的维度。常见字段包括:name(调试时定位用)、input(传给被测函数的参数)、want(期望返回值)、err(期望错误)。不要硬编码类型,用具体业务类型更安全。
func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
want time.Duration
err bool
}{
{"zero", "0s", 0, false},
{"seconds", "30s", 30 * time.Second, false},
{"invalid", "1y2d", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := time.ParseDuration(tt.input)
if (err != nil) != tt.err {
t.Errorf("ParseDuration(%q) error = %v, wantErr %v", tt.input, err, tt.err)
return
}
if !tt.err && got != tt.want {
t.Errorf("ParseDuration(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}-
t.Run(tt.name, ...)让每个子测试独立显示,失败时能直接看到是哪个name挂了 - 判断错误是否符合预期,用
(err != nil) != tt.err比err == nil != tt.err更清晰,避免布尔逻辑翻车 - 只有非错误路径才比对
got和want,否则got可能未定义或无意义
什么时候该拆出独立 struct:当字段多、复用强、或要导出时
如果测试表在多个文件或多个函数间共用,或者字段超过 4–5 个(比如还要加 setup 函数、teardown、skip 标志),建议定义具名 struct。这样 IDE 能补全字段,也方便加方法或实现 fmt.Stringer。
type parseTest struct {
input string
want int
err error
}
func (p parseTest) String() string {
return fmt.Sprintf("parseTest{input:%q,want:%d}", p.input, p.want)
}
func TestParseInt(t testing.T) {
tests := []parseTest{
{"42", 42, nil},
{"abc", 0, errors.New("invalid syntax")},
}
for _, tt := range tests {
t.Run(tt.String(), func(t testing.T) {
got, err := strconv.Atoi(tt.input)
if !errors.Is(err, tt.err) {
t.Errorf("Atoi(%q): got error %v, want %v", tt.input, err, tt.err)
return
}
if got != tt.want {
t.Errorf("Atoi(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
- 用
errors.Is()而不是==比较错误,支持包装错误(fmt.Errorf("wrap: %w", err)) -
String()方法让t.Run的名字可读性更强,尤其在大量用例时 - 别在 struct 里塞函数类型字段(如
setup func()),会破坏可序列化和打印能力;改用闭包或外部辅助函数
容易踩的坑:闭包变量捕获、并发竞争、基准测试混用
表驱动测试写顺手后,最容易在细节上翻车。下面三个问题出现频率高,且调试成本大。
立即学习“go语言免费学习笔记(深入)”;
- 循环变量复用:所有
t.Run里的tt实际指向同一个地址,必须用tt := tt显式复制,否则最后所有子测试都跑最后一个用例 - 并发测试误用:如果测试里启动 goroutine 并修改共享变量(如全局 map 或计数器),又没加锁或同步,
go test -race会报 data race —— 表驱动本身不并发,但t.Parallel()开启后就真并发了 - 误把
Benchmark当Test写:表结构可以复用,但testing.B和testing.T方法完全不同,比如b.Error()不存在,b.ReportAllocs()也不能在Test里调
最常被忽略的是子测试命名唯一性:如果 name 字段生成逻辑有 bug(比如全为空字符串或重复),go test 会静默跳过后续同名测试,而不是报错 —— 这时候得靠 go test -v 看实际运行了几个子测试。











