表驱动测试是Go官方推荐的单元测试组织方式,核心是用结构体切片定义测试用例并用for循环统一执行断言;需为每个case显式复制循环变量tt以避免闭包陷阱,适用于输入→处理→输出/错误链条高度一致的场景。

什么是表驱动测试(table-driven test)
Go 语言官方推荐的单元测试组织方式,核心是把测试用例抽象成结构体切片,用 for 循环统一执行断言。它不是语法特性,而是一种约定俗成的模式,能显著减少重复代码、提升可读性和维护性。
如何定义测试用例表
用一个匿名或具名结构体切片保存输入、期望输出、描述等字段。字段名建议用 name、input、expected、err 等通用命名,避免过度定制化导致后续难复用。
-
name字段必须有,用于t.Run()的子测试名,失败时能快速定位是哪个 case - 不要把断言逻辑(如
reflect.DeepEqual)塞进结构体字段,保持数据和逻辑分离 - 如果某个字段在多数 case 中相同,可用零值 + 显式覆盖,不必为“默认值”单独设字段
func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
err bool
}{
{"empty", "", 0, true},
{"valid_ms", "100ms", 100 * time.Millisecond, false},
{"invalid", "100xyz", 0, true},
}
for _, tt := range tests {
tt := tt // 必须显式复制,防止闭包引用循环变量
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDuration(tt.input)
if (err != nil) != tt.err {
t.Fatalf("ParseDuration(%q) error = %v, wantErr %v", tt.input, err, tt.err)
}
if !tt.err && got != tt.expected {
t.Errorf("ParseDuration(%q) = %v, want %v", tt.input, got, tt.expected)
}
})
}
}
为什么必须写 tt := tt
这是 Go 单元测试里最常被忽略的陷阱。如果不复制,所有子测试闭包捕获的是同一个循环变量地址,最终所有 t.Run 执行时 tt 都指向最后一次迭代的值,导致全部 case 行为一致甚至 panic。
- Go 1.22+ 对这种场景有静态分析警告,但旧版本不会报
- 即使结构体很小,也必须复制;不能靠“我这个结构体没指针就没事”侥幸
- 复制语句要放在
t.Run外层,不能放 inside —— 否则无法解决闭包问题
什么时候不适合用表驱动
不是所有测试都适合。当用例之间依赖状态、需要顺序执行、或每个 case 要 mock 不同行为时,强行塞进表里反而增加复杂度。
立即学习“go语言免费学习笔记(深入)”;
- 涉及
time.Sleep或真实 I/O 的测试(比如测试超时),应拆成独立函数 - 需要多次调用同一函数并验证中间状态(如状态机流转),用传统逐条写更清晰
- 某个 case 的 setup/cleanup 过程远比其他 case 复杂,单独拎出来可读性更好
表驱动的价值在于“同构性”——输入 → 处理 → 输出/错误 的链条高度一致。一旦这个链条开始变形,就该停下来想是不是走错路了。











