Go

How do you test time-dependent concurrent code deterministically? Explain testing/synctest (Go 1.25).

Question 477HardGo 1.22 to 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

All 35 Standard Library, HTTP & Systems Design in Go questions