原文链接:《使用 testing/synctest 测试并发代码》
这篇文章《使用 testing/synctest 测试并发代码》由 Damien Neil 撰写,发表于 2025 年 2 月 19 日,介绍了 Go 1.24 中引入的实验性包 testing/synctest
,旨在简化并发代码的测试。
🤔 并发程序测试的挑战
Go 语言以其内置的并发支持(如 goroutine 和 channel)而闻名,但测试并发程序却常常困难且容易出错。
以 context.AfterFunc
为例,该函数在上下文被取消后,在新的 goroutine 中调用指定的函数。测试其行为时,我们希望验证:
- 在上下文取消之前,函数未被调用。
- 在上下文取消之后,函数被调用。
传统的测试方法可能如下:
func TestAfterFunc(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
calledCh := make(chan struct{})
context.AfterFunc(ctx, func() {
close(calledCh)
})
// 检查函数未被调用
select {
case <-calledCh:
t.Fatalf("函数在上下文取消前被调用")
case <-time.After(10 * time.Millisecond):
// 预期路径
}
cancel()
// 检查函数已被调用
select {
case <-calledCh:
// 预期路径
case <-time.After(10 * time.Millisecond):
t.Fatalf("函数在上下文取消后未被调用")
}
}
这种方法的问题在于:
- 速度慢:每次测试都需要等待一定时间。
- 不稳定:在资源受限的环境中(如 CI 系统),可能出现偶发失败。
🧪 引入 testing/synctest 包
为了解决上述问题,Go 1.24 引入了实验性的 testing/synctest
包。该包提供了两个主要函数:
Run(f func())
:在新的 goroutine 中执行函数f
,并创建一个隔离的“气泡”环境。Wait()
:等待当前气泡中的所有 goroutine 都处于阻塞状态。
使用 synctest
,上述测试可以重写为:
func TestAfterFunc(t *testing.T) {
synctest.Run(func() {
ctx, cancel := context.WithCancel(context.Background())
called := false
context.AfterFunc(ctx, func() {
called = true
})
synctest.Wait()
if called {
t.Fatalf("函数在上下文取消前被调用")
}
cancel()
synctest.Wait()
if !called {
t.Fatalf("函数在上下文取消后未被调用")
}
})
}
这种方法的优点包括:
- 速度快:无需实际等待时间。
- 稳定性高:避免了由于系统负载导致的测试不确定性。
- 无需修改被测试代码:测试逻辑与业务逻辑解耦。
⏱️ 虚拟时间的优势
synctest
使用虚拟时间来控制测试中的时间相关操作。在气泡环境中,time.Sleep
等函数不会实际等待,而是模拟时间的流逝。这使得测试更加高效和可预测。
🌐 测试网络代码
synctest
也适用于测试网络相关的并发代码,例如验证 net/http
包在处理 100 Continue
响应时的行为。通过模拟时间和控制 goroutine 的调度,可以更精确地测试网络协议的实现。
🚧 实验状态
testing/synctest
是 Go 1.24 中引入的实验性包,默认未启用。要使用它,需要在编译时设置环境变量:(Go)
GOEXPERIMENT=synctest
该包的 API 可能在未来版本中发生变化,建议仅在测试环境中使用。
📚 结语
testing/synctest
提供了一种新的方式来测试并发代码,解决了传统方法中速度慢、不稳定的问题。通过引入隔离的气泡环境和虚拟时间,测试变得更加高效和可靠。虽然目前仍处于实验阶段,但它为 Go 的并发测试提供了有力的工具。