Skip to content

Commit

Permalink
engineering: go commentary in dec 13 (#451)
Browse files Browse the repository at this point in the history
  • Loading branch information
fuatto authored Dec 13, 2024
1 parent ffdc3cf commit 2d60272
Showing 1 changed file with 153 additions and 0 deletions.
153 changes: 153 additions & 0 deletions go/weekly/dec-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
tags:
- golang
- go-weekly
authors:
- fuatto
title: 'Go Commentary #24: Coming in Go 1.24: testing/synctest experiment for time and concurrency testing'
short_title: '#24 Go 1.24 testing/synctest experiment for time and concurrency testing'
description: Go 1.24 testing/synctest experiment for time and concurrency testing

date: 2024-12-13
---

## [Coming in Go 1.24: testing/synctest experiment for time and concurrency testing](https://danp.net/posts/synctest-experiment/)

### Context

```go
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

```go
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.

```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](https://pkg.go.dev/golang.org/dl/gotip) with `GOEXPERIMENT=synctest`

### Extending to concurrency

```go
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

```go
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/+/630382](https://go-review.googlesource.com/c/go/+/630382)

- You can try it yourself now by using `gotip` and setting `GOEXPERIMENT=synctest`. When Go 1.24 comes out GOEXPERIMENT=synctest will still be required.

- Review the [main proposal](https://github.com/golang/go/issues/67434) and share any experience you have.

---

https://danp.net/posts/synctest-experiment/

https://go-review.googlesource.com/c/go/+/630382

https://github.com/golang/go/issues/67434

0 comments on commit 2d60272

Please sign in to comment.