Featured image of post Go 1.24: 新的官方库 synctest

Go 1.24: 新的官方库 synctest

Go 1.24 引入了 `testing/synctest`,利用虚拟时钟简化了并发代码测试,从而实现更快、更可靠的测试!🚀 #GoLang #测试

 

在Go语言中,测试并发代码一直是一个具有挑战性的任务。尤其是测试一些需要跟时间相关的case的时候,传统的测试方法通常依赖于真实的系统时钟和同步机制,这会导致测试变得缓慢且容易出现不确定性。

假设我们需要测试go-cache 的过期删除功能,我们可能会写出这种代码

go-cache是一个非常流行的 Go 缓存库,支持过期时间(TTL)和定期清理过期的缓存项。它适用于缓存数据,能够自动删除过期的条目。

list1: TestGoCacheEntryExpires

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func TestGoCacheEntryExpires(t *testing.T) {  
    c := cache.New(5*time.Second, 10*time.Second)  
    c.Set("foo", "bar", cache.DefaultExpiration)  
    v, found := c.Get("foo")  
    assert.True(t, found)  
    assert.Equal(t, "bar", v)  
    time.Sleep(5 * time.Second)  
    v, found = c.Get("foo")  
    assert.False(t, found)  
    assert.Nil(t, v)  
}

运行这个case 需要等待5s,让 cache过期。

1
2
3
➜  synctest git:(main)time go test go-cacha_test.go
ok      command-line-arguments  5.012s
go test go-cacha_test.go  0.38s user 1.27s system 27% cpu 6.049 total

Golang 1.24 之后,如何使用testing/synctest 解决这个问题呢?首先看看基于 testing/synctest 如何写这个test case 把。
List 2: TestGoCacheEntryExpiresWithSynctest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func TestGoCacheEntryExpiresWithSynctest(t *testing.T) {  
    c := cache.New(2*time.Second, 5*time.Second)  
    synctest.Run(func() {  
       c.Set("foo", "bar", cache.DefaultExpiration)  
       // Get an entry from the cache.  
       if got, exist := c.Get("foo"); !exist && got != "bar" {  
          t.Errorf("c.Get(k) = %v, want %v", got, "bar")  
       }  
  
       // Verify that we get the same entry when accessing it before the expiry.  
       time.Sleep(1 * time.Second)  
       if got, exist := c.Get("foo"); !exist && got != "bar" {  
          t.Errorf("c.Get(k) = %v, want %v", got,"bar")  
       }  
       // Wait for the entry to expire and verify that we now get a new one.  
       time.Sleep(3 * time.Second)  
       if got, exist := c.Get("foo"); exist {  
          t.Errorf("c.Get(k) = %v, want %v", got, nil)  
       }  
    })  
}

运行时间:

1
2
3
4
➜  synctest git:(main)time GOEXPERIMENT=synctest gotip  test -run TestGoCacheEntryExpiresWithSynctest
PASS
ok      blog-example/go/go1.24/synctest 0.009s
GOEXPERIMENT=synctest gotip test -run TestGoCacheEntryExpiresWithSynctest  0.36s user 1.23s system 171% cpu 0.933 total

仅仅耗时 0.009s

testing/synctest 揭秘

testing/synctest主要目的是为了简化并发代码的测试。它的核心思想是通过使用虚拟时钟和goroutine组(也称为bubble)来控制并发代码的执行,从而使测试既快速又可靠。它仅仅只有两个接口。

1
2
func Run(f func()) { synctest.Run(f) } 
func Wait() { synctest.Wait() }

Run函数在一个新的goroutine中执行f函数,并创建一个独立的bubble,确保所有相关的goroutine都在虚拟时钟的控制下执行。

Wait 用来同步 bubblegoroutine‘s 的状态。调用该函数调用后,主goroutine将阻塞,直到bubble中的其他goroutine都处于durably blocked 状态。
durably blockedbubblegoroutine 的一个特有的状态,有几种方式可以使 goroutine 进入 durably blocked 状态。

  • bubble内向channel发送或接收数据
  • select语句中,每个case都是bubble内的channel
  • A send or receive on a channel from within the bubble
  • A select statement where every case is a channel within the bubble
  • sync.Cond.Wait
  • time.Sleep
    但是如果goroutine 是因为 系统调用外部事件而阻塞, 它就不是 durably blocked 状态,synctest.Wait 也不会因为它而阻塞。

每个bubble都有一个虚拟时钟,从2000-01-01 00:00:00 UTC 开始. 这个虚拟时钟不会根据代码的执行时间前进,只有在所有goroutine都处于空闲状态时才会向前推进。在TestGoCacheEntryExpiresWithSynctest 中调用time.Sleep 的时候, goroutine 只有一个并且处于空闲状态,时间会快速前进,而不必等待真实时间过去,所以测试运行速度很快。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func TestSynctest(t *testing.T) {  
    synctest.Run(func() {  
       before := time.Now()  
       fmt.Println("before", before)  
       f1:=func() {  
          count := 0  
          for i := 0; i < 10e9; i++ { // time consuming, It's about 3s in my machine  
             count++  
          }  
       }
       go f1()
       synctest.Wait()  //wait f1
       after := time.Now()  // time is not affected by the running time of f1
       fmt.Println("after", after)  
    })  
}

运行结果为:

1
2
3
4
5
6
➜  synctest git:(main)time GOEXPERIMENT=synctest gotip  test -run TestSynctest 
before 2000-01-01 08:00:00 +0800 CST m=+946164282.733407335
after 2000-01-01 08:00:00 +0800 CST m=+946164282.733407335
PASS
ok      blog-example/go/go1.24/synctest 3.131s
GOEXPERIMENT=synctest gotip test -run TestSynctest  3.45s user 1.25s system 133% cpu 3.533 total

在 gosrc 中已经已经有蛮多使用 synctest 优化case 的例子。

Pasted image 20241223173803

参考资料:
https://github.com/golang/go/blob/05d8984781f7cf2f0f39b53699a558b6a1965c6c/src/testing/synctest/synctest.go#L41

Licensed under CC BY-NC-SA 4.0
最后更新于 Dec 19, 2024 19:22 CST
使用 Hugo 构建
主题 StackJimmy 设计