How do you test time-dependent concurrent code deterministically? Explain testing/synctest (Go 1.25).
Tests using real time.Sleep are slow and flaky. testing/synctest (experimental in 1.24 behind GOEXPERIMENT=synctest, stable in Go 1.25 as synctest.Test) runs a function in an isolated bubble with a fake clock. Time only advances when every goroutine in the bubble is durably blocked (on channels, time.Sleep, sync.WaitGroup, sync.Cond, select) — then the clock jumps instantly to the next timer. synctest.Wait() blocks until all other bubble goroutines are durably blocked, giving a deterministic "everything has settled" point.
func TestContextTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
time.Sleep(5*time.Second - time.Nanosecond) // returns instantly
synctest.Wait()
if err := ctx.Err(); err != nil {
t.Fatalf("too early: %v", err)
}
time.Sleep(time.Nanosecond)
synctest.Wait()
if ctx.Err() != context.DeadlineExceeded {
t.Fatalf("want DeadlineExceeded, got %v", ctx.Err())
}
})
}
Gotchas: blocking on I/O (real network sockets, syscalls) or a sync.Mutex is not durable blocking, so use net.Pipe or in-memory fakes instead of real listeners; the bubble starts at a fixed fake time (2000-01-01 UTC); goroutines and channels created in a bubble must not be used from outside it; if all goroutines block forever with no pending timers, the test fails with a deadlock rather than hanging. The alternative before 1.25 was injecting a clock interface.
More on Standard Library, HTTP & Systems Design in Go
- Q475Write an HTTP client retry helper with exponential backoff and jitter. What makes a request safe to retry?
- Q476Build a reverse proxy with httputil.ReverseProxy. Why is Rewrite preferred over Director?