表驱动测试是go标准写法,需用具名结构体封装用例、t.run正确嵌套防闭包陷阱、显式验证err、避免init阶段耗时计算。

表驱动测试在 Go 里不是“可选技巧”,而是标准写法——testing 包天然鼓励你把测试用例组织成结构体切片,而不是写一堆重复的 TestXxx 函数。
为什么用 struct{input, want, name string} 而不是多个变量?
Go 的测试函数本身不支持参数化,所以得靠手动循环。用结构体封装输入、期望、名称,能让每个用例自解释,也方便加字段(比如 shouldPanic 或 timeout)。如果拆成平行变量,新增一个维度(如是否忽略大小写)就得改所有测试逻辑,容易漏。
常见错误:用 map[string]interface{} 存测试数据——类型丢失,IDE 不提示,运行时报 panic: interface conversion。
- 始终用具名结构体,哪怕只有一两个字段
- 字段名保持一致(推荐
name,input,want,err) - 给结构体加
String()方法(非必须,但调试时t.Log(tc)更清晰)
t.Run() 必须嵌套在循环内,且传入 tc.name
不调用 t.Run(),所有用例就跑在同一个测试上下文里:失败时无法定位到具体 case,也无法单独运行某个子测试(go test -run=TestParseJSON/invalid_json 就失效)。
立即学习“go语言免费学习笔记(深入)”;
典型坑:for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { ... }) } 写对了,但闭包里直接用了 tc 变量——所有子测试实际共享最后一次循环的 tc 值。
- 正确写法:在
t.Run的函数参数里重新声明变量,例如t.Run(tc.name, func(t *testing.T) { tc := tc; ... }) - 或更简洁:用索引访问
tests[i],避免闭包陷阱 - 名称字符串别含空格或斜杠,否则
-run过滤可能意外匹配失败
错误处理类测试要显式检查 err 字段,别只比 got 和 want
很多函数返回 (result, error),但新手常只断言 result,漏掉对 err 的验证。比如期望出错,结果却返回了 nil 错误,测试照样通过。
示例场景:解析时间字符串,"2023-13-01" 应该返回非 nil err,但若测试只写 if got != tc.want { t.Errorf(...) },就完全绕过了错误路径。
- 每个测试用例结构体加
err bool字段(表示是否期望错误),或更明确地用errType reflect.Type - 执行后先检查
if (err != nil) != tc.err { ... },再检查result - 用
errors.Is(err, xxxErr)替代err == xxxErr,兼容包装错误
性能敏感场景下,避免在表中预计算耗时值
表驱动测试的结构体在包初始化时就构造完成。如果某个 tc.input 是 10MB 的 JSON 字符串,或 tc.want 是调用一次 heavyComputation() 得到的结果,会导致测试二进制体积膨胀、启动变慢,甚至因 init 阶段 panic 导致整个测试包加载失败。
真实踩坑:有人把 tc.want 设为 encrypt([]byte("hello")) 的结果,结果加密库在 init 时依赖未初始化的全局状态,测试直接 crash。
- 所有耗时或有副作用的计算,放到
t.Run内部做 - 大文本/二进制数据用
testdata/目录存放,测试时读取 - 如果必须复用中间结果(如解析后的 AST),用
sync.Once懒加载,而非塞进表里
表驱动测试真正的复杂点不在语法,而在于怎么让 tests 切片既覆盖边界又不爆炸——比如时间解析要测 UTC、本地时区、带毫秒、无年份等十几种组合,这时候得靠代码生成或辅助函数构造用例,而不是手敲。











