SeriesPart 8 of 9 // Go 1.24
GoWriting
Feb 9, 2025
3 min read
Concurrency

Advanced Concurrency Testing in Go 1.24: Exploring testing/synctest

Go 1.24 introduces the new testing/synctest package, an experimental feature designed to improve testing for concurrent code. This package…

Go 1.24 introduces the new testing/synctest package, an experimental feature designed to improve testing for concurrent code. This package…

Advanced Concurrency Testing in Go 1.24: Exploring **testing/synctest**

Go 1.24 introduces the new testing/synctest package, an experimental feature designed to improve testing for concurrent code. This package provides mechanisms to isolate and observe goroutines in a controlled environment, helping developers detect subtle race conditions, deadlocks, and concurrency issues that might otherwise go unnoticed.

Why testing/synctest?

Concurrency bugs can be notoriously difficult to detect and reproduce due to their non-deterministic nature. Traditional testing methods often fail to expose these issues reliably. The testing/synctest package addresses this challenge by introducing tools to:

  • Isolate goroutines to test them in a controlled environment.
  • Manipulate time in concurrent tests to ensure predictable execution.
  • Detect blocking conditions and deadlocks before they manifest in production.

Key Features of testing/synctest

1. Manipulating Time for Predictable Tests

Traditional time-based tests rely on real-world timing, making them unreliable. synctest.Run ensures that time progresses predictably within a controlled test environment.

Example: Ensuring Consistent Timing

package synctest
 
import (
  "testing"
  "testing/synctest"
  "time"
)
 
func TestTime3(t *testing.T) {
  synctest.Run(func() {
    before := time.Now()
    time.Sleep(10 * time.Second)
    after := time.Now()
    if d := after.Sub(before); d != 10*time.Second {
      t.Fatalf("took %v", d)
    } else {
      t.Logf("took %v", d)
    }
  })
}

2. Testing Concurrent Execution with synctest.Run

synctest.Run enables controlled execution of concurrent code, ensuring goroutines do not behave unpredictably.

Example: Ensuring Controlled Execution

package synctest
 
import (
  "context"
  "sync/atomic"
  "testing"
  "testing/synctest"
  "time"
)
 
func TestConcurrency2(t *testing.T) {
  synctest.Run(func() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    tick := time.NewTicker(time.Millisecond)
    var hits atomic.Int32
    go func() {
      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)
    }
  })
}

Use Cases for testing/synctest

  • Testing concurrent algorithms: Ensure correctness under various execution conditions.
  • Detecting deadlocks early: Identify blocked goroutines before they impact production.
  • Ensuring predictable time-based behaviour: Eliminate flakiness in time-dependent tests.

Conclusion

The testing/synctest package in Go 1.24 provides powerful new tools for testing concurrent applications. By allowing controlled goroutine isolation, blocking detection, and time manipulation, this package makes it easier to identify and fix concurrency-related bugs.

Since testing/synctest is currently experimental, developers must enable it explicitly using:

GOEXPERIMENT=synctest go test ./...

Stay tuned for our next post, where we explore enhancements to the Go toolchain in Go 1.24.

By Ajitem Sahasrabuddhe on February 9, 2025.

Series contents