表驱动测试是Go中用结构体切片定义测试用例、通过t.Run逐个执行的惯用法;需明确name字段、避免tt闭包陷阱、正确校验error,不适用于逻辑差异大或I/O密集场景。

什么是表驱动测试(table-driven test)
Go 语言没有内置的参数化测试机制,但用切片 + for 循环模拟“数据驱动”是最自然、最被社区广泛接受的方式。它把测试输入、预期输出、描述信息组织成结构体切片,逐条执行断言——不是语法糖,而是 Go 的惯用法。
怎么写一个基础的表驱动测试
核心是定义测试用例结构体、填充 tests 切片、遍历调用被测函数并比对结果。注意:每个测试用例必须有唯一可读的 name 字段,便于定位失败项。
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
-
t.Run()是关键:它为每个子测试创建独立作用域,支持并发运行、单独重试、清晰的失败路径 - 结构体字段名要语义明确,避免用
input/output这类泛称,比如用username、expectedErr更易读 - 不要在循环里直接用
tt变量做闭包(常见坑!),必须传入t.Run的匿名函数内或显式复制,否则所有子测试共享最后一个tt值
如何处理错误和边界情况
真实函数常返回 (result, error),表驱动测试需同时校验值与错误。建议用指针比较错误(errors.Is 或 errors.As),而非字符串匹配。
func TestParseInt(t *testing.T) {
tests := []struct {
name string
input string
expected int
expectedErr error
}{
{"valid", "42", 42, nil},
{"empty", "", 0, strconv.ErrSyntax},
{"space", " 123 ", 123, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := strconv.Atoi(tt.input)
if !errors.Is(err, tt.expectedErr) {
t.Errorf("Atoi(%q) error = %v, want %v", tt.input, err, tt.expectedErr)
return
}
if got != tt.expected {
t.Errorf("Atoi(%q) = %d, want %d", tt.input, got, tt.expected)
}
})
}
}
- 当
expectedErr是具体错误类型(如io.EOF),用errors.Is(err, io.EOF) - 若需检查错误是否包含特定字段(如自定义错误的
Code),用errors.As(err, &target) - 别忽略
err == nil和expectedErr == nil的双重判断逻辑,漏判会导致 panic 或误通过
什么时候不该用表驱动测试
不是所有场景都适合。当测试逻辑差异大、setup/teardown 成本高、或需要精细控制执行顺序时,硬塞进一张表反而增加维护负担。
立即学习“go语言免费学习笔记(深入)”;
- 涉及真实 I/O(如打开文件、调用 HTTP API):每个 case 都要 mock 或清理,不如拆成独立测试函数
- 状态依赖强(如测试一个对象生命周期:初始化 → 修改 → 序列化 → 反序列化):表结构难以表达步骤链
- 某个 case 需要特殊调试手段(如加
log.Printf或断点),混在循环里会干扰其他 case - 用例数量极少(t.Run 更直白
真正容易被忽略的是:表驱动测试的可读性完全依赖于结构体字段命名和 name 描述质量。一个叫 test1 的 case 失败时,你得翻源码才能知道它到底在测什么。











