测试超时、重试、定时器这类依赖时间的并发代码,以前只能真的睡;或者把时间抽象成接口再注入假时钟。testing/synctest 让 time 包在一个“气泡”里用假时钟,所有 goroutine 都阻塞时时间才快进。这篇用一个 TTL 一小时的缓存和一个轮询函数实测它,并验证死锁检测和泄漏检测。
TestRealTTL:通过,测试自己报告耗时 0.35 秒 TestFakeTTL:通过,测试自己报告耗时 0.00 秒;c_test.go:28: 起点 2000-01-01 08:00:00 CST;测试里过去了 1h0m1s(假时钟) (TestRealTTL 用真实时钟,TTL 只能设 300 ms;TestFakeTTL 的 TTL 是 1 小时,在 synctest 的假时钟里瞬间完成)
气泡里的 time.Sleep(59*time.Minute + 59*time.Second) 瞬间完成,time.Since(start) 报告 1h0m1s,测试本身耗时 0.00 秒。真实时钟版本为了不让测试太慢,只能把 TTL 缩到 300 ms 去测——测的就不是生产参数了。
c_test.go:37: 每秒轮询一次、超时 10.001 秒:调用了 10 次 | --- PASS: TestFakePoll (0.00s) 同一个测试连续重复 50 次(-count=50):通过 50 次,失败 0 次
假时钟只在气泡里所有 goroutine 都阻塞时才推进,所以每秒一次、10.001 秒超时的轮询,调用次数永远是 10,没有调度抖动的影响。真实时钟下的同类测试通常要放宽断言(本文没有统计真实时钟下的偏差)。
=== RUN TestFakeWait | c_test.go:49: synctest.Wait() 返回之后,后台 goroutine 已经执行完:done=true | --- PASS: TestFakeWait (0.00s)
synctest.Wait() 等其它 goroutine 都“持久阻塞”或结束再返回,所以不需要靠 time.Sleep 去猜后台 goroutine 跑完没有。
退出码 1;--- FAIL: TestDeadlock (0.00s) | panic: deadlock: all goroutines in bubble are blocked [recovered, repanicked] | panic({0x10069d3a0?, 0x79a587928108?}) | <GOROOT>/src/runtime/panic.go:859 +0x120 | lab.TestDeadlock(0x79a587966248?) | lab.TestDeadlock.func1(0x79a587966488?)气泡里所有 goroutine 都阻塞且没有定时器可推进,synctest.Test 直接 panic deadlock: all goroutines in bubble are blocked;根 goroutine 退出时还有阻塞的 goroutine,panic main bubble goroutine has exited but blocked goroutines remain。副作用是测试必须把自己启动的 goroutine 都收拾干净。
退出码 1;--- FAIL: TestLeakExit (0.00s) | panic: deadlock: main bubble goroutine has exited but blocked goroutines remain [recovered, repanicked] | goroutine 5 [running]: | testing.tRunner.func1.2({0x10108d3a0, 0x34aa94bb6120}) | <GOROOT>/src/testing/testing.go:2123 +0x1a0 | testing.tRunner.func1() | <GOROOT>/src/testing/testing.go:2126 +0x2c8 | panic({0x10108d3a0?, 0x34aa94bb6120?}) | <GOROOT>/src/runtime文档的清单是:气泡内创建的 channel 上的阻塞操作、sync.Cond.Wait、WaitGroup.Wait(Add 在气泡内)和 time.Sleep。网络、外部进程、不在气泡内创建的 channel 等不算,所以文档建议测试自包含、避免真实网络。我没有试 sync.Mutex 之类不在清单里的操作的实际表现。
The following operations durably block a goroutine: - a blocking send or receive on a channel created within the bubble - a blocking select statement where every case is a channel created within the bubble - sync.Cond.Wait - sync.WaitGroup.Wait, when sync.WaitGroup.Add was called within the bubble - time.Sleep Operations not in the above list are not durably blocking. In particular, the following operations may block a goroutine, but are not durably blocking because the goroutine can be unblocked by an event occurring outside its bubble:
文档建议每个测试自包含:不与气泡外的 goroutine 交互、不用网络(用假网络)、不依赖外部进程、不泄漏 goroutine。