必须用 t.run 而不是多个 testxxx 函数,因其支持共享初始化、独立失败、层级命名、精准筛选、生命周期复用及并发控制;循环中需显式拷贝变量(tc := tc)避免闭包陷阱。

为什么必须用 t.Run 而不是写一堆 TestXxx 函数
因为共享初始化逻辑。比如你要测一个 HTTP handler,每次测试都要启动 mock server、准备临时数据库连接、设置环境变量——如果每个 TestHandlerXXX 都重复做一遍,既慢又容易出错;而用 t.Run,你只在顶层 TestHandler 里 setup 一次,所有子测试复用同一套资源。
- 子测试失败不影响其他子测试运行,
TestXxx里用t.Fatal会直接中断整个函数 - 错误路径带层级名,比如
TestHandler/Auth/valid_token,一眼定位到哪条分支挂了 - 支持
go test -run=TestHandler/Auth精准筛选,调试时不用反复改代码删用例 - 父子测试共用生命周期(如
t.Cleanup()可在父级注册,子测试自动继承),但各自有独立的*testing.T实例,状态不污染
t.Run 的命名和闭包陷阱怎么避
名字不能含 /(会被解析为嵌套层级,但手动拼接易出错),也不能为空或重复;循环中创建子测试时,若不显式拷贝变量,所有子测试会看到同一个值——这是最常踩的坑。
- 命名建议用小写+下划线,比如
"empty_input"、"with_timeout",别用"case1"或"test_a" - 循环中必须写
tc := tc再传进t.Run,否则闭包捕获的是循环变量地址,最后所有子测试都跑最后一个用例 - 子测试函数体里调用
t.Parallel()必须放在第一行,否则无效;且仅当该子测试被单独运行或父测试未阻塞时才真正并发 - 不要在子测试里调
t.Helper()以外的辅助函数而不标记 helper,否则错误堆栈指向调用点而非断言行
表格驱动 + t.Run 怎么写才干净
这是最主流的用法:把测试用例定义成结构体切片或 map,遍历生成子测试。好处是增删用例不改主干逻辑,结构清晰,日志可读性强。
- 用
struct{ name string; input ...; want ... }比裸 map 更类型安全,字段名即文档 - 子测试名推荐用
fmt.Sprintf("Add(%d,%d)", tc.a, tc.b)这类动态生成,比硬编码更易维护 - 重型初始化(如建 DB 连接、启 mock server)提到
t.Run外,轻量 setup(如构造请求对象)放子测试内,配defer清理 - 避免在子测试里做耗时操作(如加载大文件、sleep),否则并行时拖慢整体速度
如何精准运行和调试单个子测试
靠 -run 参数加名称匹配,这是子测试最大实操价值:不用注释代码、不用拆文件、不用重启 IDE,命令行敲完就跑指定用例。
立即学习“go语言免费学习笔记(深入)”;
- 运行某组:
go test -run=TestParseURL/json(匹配所有以json开头的子测试名) - 运行单个:
go test -run=TestParseURL/valid_http,支持模糊匹配,go test -run=valid也能命中 - 加
-v查看每个子测试的开始/结束时间,卡住时能快速识别是哪个子测试 hang 住 - 注意:子测试名区分大小写,且不支持正则,只支持前缀匹配和斜杠分组
if !ok { t.Fatal() } 堆满一个函数,其实只要把每条分支包进 t.Run,再补上那句 tc := tc,整个测试就从“能跑”变成“好调、好扩、好读”。最常漏掉的,就是那个显式变量拷贝。










