Go 1.28 的 synctest.Subtest:把 t.Run 和 synctest.Test 合成一层
Go 1.28 新增 testing/synctest.Subtest,对应提案 golang/go#77320,该提案已接受。
synctest.Test 在 Go 1.25 已经 GA。它把函数放进隔离的 bubble 中运行。bubble 内的 time 包使用假时钟,启动的 goroutines 也属于这个 bubble。synctest.Wait 会阻塞,直到 bubble 内除当前 goroutine 外的所有 goroutine 都处于 durably blocked。synctest.Sleep(d) 等价于 time.Sleep(d) 再加一次 Wait()。这些机制让依赖时间和并发的测试可以在受控时钟下推进。
Subtest 的实现很直接:
func Subtest(t *testing.T, name string, f func(*testing.T)) {
t.Helper()
t.Run(name, func(t *testing.T) {
t.Helper()
Test(t, f)
})
}
它等价于调用 t.Run,并在其中调用 Test。也就是把子测试的注册和 bubble 的创建放在同一个函数里。
动机来自缩进。单独使用 synctest.Test 会引入一层缩进;testing.T.Run 又引入一层;表格驱动测试本身通常还有一层循环和子测试。三层叠起来后,闭包里的测试体往往要往右移很多,阅读和修改都变得别扭。把 t.Run 与 synctest.Test 合并后,可以去掉一层缩进。
另一个动机是手写 helper 容易复制粘贴出错。一个容易出现的错误是:
func TestB(t *testing.T) {
synctest.Test(t, testA)
}
这段代码看起来是在给 TestB 创建 bubble,但实际跑的是 testA。如果测试很多,这种错误不一定立刻暴露,可能表现为覆盖了错误的行为,或者两个测试跑了同一段逻辑。Subtest 让调用点直接写明名字和函数,减少这种复制粘贴错误。
前后对比如下。
没有 Subtest 时,表格驱动测试里常见写法是:
func TestFoo(t *testing.T) {
tests := []struct {
name string
f func(t *testing.T)
}{
{"case a", testA},
{"case b", testB},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
synctest.Test(t, tt.f)
})
}
}
这里 t.Run 一层,synctest.Test 又一层。每个用例都要在两层闭包里执行。
使用 Subtest 后:
func TestFoo(t *testing.T) {
tests := []struct {
name string
f func(t *testing.T)
}{
{"case a", testA},
{"case b", testB},
}
for _, tt := range tests {
synctest.Subtest(t, tt.name, tt.f)
}
}
t.Run(tt.name, func(t *testing.T) { synctest.Test(t, tt.f) }) 变成 synctest.Subtest(t, tt.name, tt.f)。子测试注册、bubble 创建、假时钟和 Wait/Sleep 的语义仍然由 t.Run 与 synctest.Test 承担,只是调用点少了一层。
命名也讨论过。有人建议叫 synctest.Run,因为和 testing.T.Run 对称,读起来更像在“运行”一个测试。最终名字是 Subtest。原因是 Run 被保留给未来的函数:那个函数预期不接收 *testing.T,直接开一个 bubble。也就是说,Run 这个名字要留给更底层的 bubble 入口,而不是现在的子测试封装。因此 Subtest 更准确地表达了它和 testing.T.Run 的关系。
关于假时钟和等待,需要区分几个调用。synctest.Test 负责把函数放进 bubble;bubble 里的 time 是假时钟。synctest.Wait 不是 sleep,它等的是 bubble 内除当前 goroutine 外所有 goroutine 都 durably blocked。synctest.Sleep(d) 则是 time.Sleep(d) 后再 Wait(),适合让假时钟前进并等待其他 goroutine 到达稳定状态。把这些写进表格驱动测试时,Subtest 不会改变底层语义,只是减少嵌套。
链接:
- 提案:golang/go#77320
- 官方博客:Testing time