Go Commentary #24: Coming in Go 1.24: testing/synctest experiment for time and concurrency testing
Coming in Go 1.24: testing/synctest experiment for time and concurrency testing
Context
func Test(t *testing.T) {
before := time.Now()
time.Sleep(time.Second)
after := time.Now()
if d := after.Sub(before); d != time.Second {
t.Fatalf("took %v", d)
}
}
- Traditional hack
func Test(t *testing.T) {
before := time.Now()
time.Sleep(time.Second)
after := time.Now()
if d := after.Sub(before); d >= 2*time.Second {
t.Fatalf("took %v", d)
}
}
- It’s still flaky because it depends on the system clock.
Solution
- The
testing/synctest
package is an experiment to provide a more deterministic way to test time and concurrency in Go.
import (
"testing"
"testing/synctest"
"time"
)
func Test(t *testing.T) {
synctest.Run(func() {
before := time.Now()
time.Sleep(time.Second)
after := time.Now()
if d := after.Sub(before); d != time.Second {
t.Fatalf("took %v", d)
}
})
}
- And then use gotip with
GOEXPERIMENT=synctest
Extending to concurrency
func Test(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
var hits atomic.Int32
go func() {
tick := time.NewTicker(time.Millisecond)
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
hits.Add(1)
}
}
}()
time.Sleep(3 * time.Millisecond)
cancel()
got := int(hits.Load())
if want := 3; got != want {
t.Fatalf("got %v, want %v", got, want)
}
}
It’s flaky because of the initial delay of the Ticker
Wrap the test in
synctest.Run
to make it deterministic
func Test(t *testing.T) {
synctest.Run(func() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
var hits atomic.Int32
go func() {
tick := time.NewTicker(time.Millisecond)
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
hits.Add(1)
}
}
}()
time.Sleep(4 * time.Millisecond)
cancel()
got := int(hits.Load())
if want := 3; got != want {
t.Fatalf("got %v, want %v", got, want)
}
})
}
Conclusion
It seems that
testing/synctest
will significantly improve testing code that involves time or concurrency. Example in go source: https://go-review.googlesource.com/c/go/+/630382You can try it yourself now by using
gotip
and settingGOEXPERIMENT=synctest
. When Go 1.24 comes out GOEXPERIMENT=synctest will still be required.Review the main proposal and share any experience you have.
https://danp.net/posts/synctest-experiment/