Go

What is testing/synctest and what problem does it solve?

Question 383HardGo 1.22 to 1.25

Testing code with timeouts, tickers, and background goroutines usually means real sleeps (slow and flaky) or injecting a fake clock everywhere. testing/synctest (experimental in Go 1.24 behind GOEXPERIMENT=synctest as synctest.Run; GA in Go 1.25 as synctest.Test) runs the test in an isolated bubble:

  • Goroutines started in the bubble belong to it.
  • The bubble has a fake clock (starting at 2000-01-01 UTC). Time advances only when every goroutine in the bubble is durably blocked — then it jumps instantly to the next timer.
  • synctest.Wait() blocks until all other bubble goroutines are durably blocked — a deterministic "let everything settle."
func TestCacheExpiry(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        c := NewCache(5 * time.Second) // starts a janitor goroutine
        defer c.Close()
        c.Set("k", "v")

        time.Sleep(4 * time.Second) // instant in fake time
        synctest.Wait()
        if _, ok := c.Get("k"); !ok {
            t.Fatal("expired too early")
        }

        time.Sleep(2 * time.Second)
        synctest.Wait()
        if _, ok := c.Get("k"); ok {
            t.Fatal("should have expired")
        }
    })
}

Gotchas: blocking on real I/O (network sockets, syscalls) or a mutex is not durable blocking, so use in-memory fakes like net.Pipe; channels created outside the bubble don't count. If all bubble goroutines deadlock, the test fails with a report instead of hanging.

More on Performance, Profiling & Testing

All 38 Performance, Profiling & Testing questions