1.25单元测试新增包testing/synctest
下面给出一段可运行的最小示例,用来体验 Go 1.25 新增的 testing/synctest 包。
代码只有 60 行,但完整展示了官方文档里提到的三大卖点:
- 隔离“气泡”——
synctest.Test把并发逻辑封进独立调度器; - 虚拟时间——
time.Sleep不再真睡,而是在所有 goroutine 阻塞时瞬间“快进”; - 等待机制——
synctest.Wait阻塞到所有 goroutine 都卡住,再断言时间。
代码:synctest_demo.go
package main
import (
"sync"
"testing"
"time"
// Go 1.25 新增包
"testing/synctest"
)
// 要测的并发函数:启动 3 个 worker,每个睡 1 s,然后累加结果。
func ConcurrentCounter(done chan<- int) {
var wg sync.WaitGroup
sum := 0
for i := 0; i < 3; i++ {
wg.Add(1)
go func(v int) {
defer wg.Done()
time.Sleep(time.Second) // 这里是关键:会触发虚拟时间
sum += v
}(i + 1)
}
wg.Wait()
done <- sum
}
// 单元测试
func TestConcurrentCounter(t *testing.T) {
// 1. 把测试关进“气泡”
synctest.Test(func(t *testing.T) {
done := make(chan int, 1)
// 2. 启动被测代码
go ConcurrentCounter(done)
// 3. 等待所有 goroutine 阻塞(这里会阻塞在 wg.Wait() 内部)
synctest.Wait()
// 4. 此时虚拟时间已经过去 1 s,但真实时间几乎没走
select {
case n := <-done:
if n != 6 { // 1+2+3
t.Fatalf("got %d; want 6", n)
}
default:
t.Fatal("counter not finished")
}
}, t) // 把外层 t 传进去,失败时能正确汇报
}
// 可执行入口,方便 go run 直接看效果
func main() {
testing.Main(func(pat, str string) (bool, error) { return true, nil },
[]testing.InternalTest{{Name: "TestConcurrentCounter", F: TestConcurrentCounter}},
nil, nil)
}运行方式(Go 1.25+):
go run synctest_demo.go -test.v输出示例(真实耗时 < 5 ms):
=== RUN TestConcurrentCounter
--- PASS: TestConcurrentCounter (0.00s)
PASS
ok xxx 0.003s代码逐段分析 & 官方特性映射
隔离“气泡”
synctest.Test(f func(*testing.T), t *testing.T)- 内部会新建一个微型调度器,所有 goroutine 只在它内部运行;
- 失败时把子失败关联到外层
t,与t.Run体验一致; - 官方文档强调“bubble”——这里正是这个术语的代码映射。
虚拟时间
被测函数里写了time.Sleep(time.Second),但在气泡内:- 时钟不会真睡;
- 只有当所有 goroutine 都阻塞(这里是
wg.Wait())时,运行时自动把虚拟时钟拨到下一个定时器到期点; - 因此 1 s 的 Sleep 瞬间过去,真实时间几乎为 0。
等待机制
synctest.Wait()- 阻塞调用者,直到当前气泡里再无可运行的 goroutine;
- 在本例里,它等到三个 worker 都执行完
sum += v并Done()之后返回; - 返回后即可安全断言结果,无需再写
time.Sleep(…)或sync.WaitGroup在测试侧再套一层。
为什么这样设计(官方意图还原)
- 传统测并发代码要么
– 真睡(慢);
– 或自己 mocktime.After/time.Sleep(侵入业务代码)。synctest把“时间”和“调度”同时虚拟化,做到零侵入 + 毫秒级。 - 过去想等“所有 goroutine 都卡住”必须手工
sync.WaitGroup或轮询,现在一句synctest.Wait()即可,测试代码更短、更确定。 - 把失败链路挂到外层
t,则go test的-run、failfast、json输出等生态完全复用,不需要新工具链。
总结
最重要的就是这一句话 “synctest 把“时间”和“调度”同时虚拟化,做到零侵入 + 毫秒级。”
不用自己开个并发然后让主线程手动等待单元测试代码运行,synctest 包能够自动的让主线程等待单元测试。
并且 synctest 包的单元测试气泡里面的虚拟时钟不会真睡,这不仅能让开发人员费心去评估 sleep 的合适时间,也能让程序运行因为不用睡眠执行效率大大提升。
RoLingG | 博客
评论(0)