必须用 t.Run 包裹每组测试用例,因其提供独立上下文、支持并行、过滤、计时与清理;闭包中需显式复制循环变量 tt := tt 避免地址复用;结构体字段应语义化命名、避免泛称和 map[string]interface{};资源初始化与清理须在子测试内完成。

Go 里写多组测试用例,不用表格驱动测试(table-driven test)不是不行,是容易写散、难定位、一改全挂——结构体切片 + t.Run 是唯一被标准库和主流项目广泛验证的写法。
为什么必须用 t.Run 包每组数据
不调用 t.Run,所有 case 共享同一个 *testing.T 实例:一个 t.Fatal 就终止整个函数,其余 case 根本没机会跑;失败时只报 TestParseJSON,看不出是哪条输入挂了;go test -run=TestParseJSON/invalid 这类子测试过滤也完全失效。
-
t.Run创建独立测试上下文,支持t.Parallel()、t.Cleanup()、单独计时和跳过 - 名字必须非空,且建议含语义,比如
"empty_input_returns_error",别用"case1" - 名字里含
/会自动分组,例如"Parse/valid"和"Parse/invalid_format"在go test -v输出里会缩进显示
for _, tt := range tests 里直接用 tt 会出什么问题
现象:所有子测试失败信息一模一样,或者只有最后一组数据通过,其余全错。根本原因是 Go 的 range 循环变量 tt 是复用的栈地址,闭包捕获的是指针,不是值。
- 错误写法:
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { use(tt) }) }—— 所有匿名函数里的tt都指向同一块内存 - 正确写法只有两种:
for _, tt := range tests { tt := tt; t.Run(...) }或改用索引访问tests[i] - 别试图在闭包里用
&tests[i]绕开——既慢又易错,IDE 提示也断掉
结构体字段怎么设计才不容易翻车
字段名不是越短越好,而是要跟被测函数签名对齐、可读、可重构。用 input expected 这种泛称,加个新参数就得改全部逻辑;用 url timeout wantStatusCode,一眼就懂,改函数时也只动对应字段。
立即学习“go语言免费学习笔记(深入)”;
- 必含字段:
name(唯一、无空格//)、输入字段(如body string)、期望字段(如wantCode int)、错误判断字段(如wantErr bool或wantErr error) - 避免
map[string]interface{}:编译期零检查,IDE 不提示,运行时 panic - 浮点比较别存
want float64,拆成wantFloat float64+tolerance float64 - 时间类输入别调
time.Now(),用固定值如time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
错误处理和清理逻辑该往哪儿放
表驱动的核心是「数据即测试」,一旦把 setup/teardown 塞进结构体或循环外,case 之间就会互相污染,t.Parallel() 失效,临时文件残留,HTTP mock 被复用——全是线上查不出的隐性 bug。
- 纯内存操作(如字符串解析):直接跑,无需初始化
- 需临时资源(如
t.TempDir()、mock HTTP server):每个子测试内自己调,用t.Cleanup()注册清理,别用defer - 外部依赖(DB、Redis):抽成工厂函数,如
newTestDB(t *testing.T) *sql.DB,每个 case 拿新实例 - 绝对不要在循环外做一次
setup()然后所有 case 共享——这是并发测试里最稳的崩溃方式
最常被忽略的一点:name 字段看似只是日志标识,实际决定了 go test -run 能不能精准命中、CI 报告能不能归因、IDE 点击失败项能不能跳转到对应数据行。起名偷懒,后面调试花三倍时间补救。



















