Go

How do you test concurrent and time-dependent code deterministically? (testing/synctest)

Question 254HardGo 1.22 to 1.25

Tests built on time.Sleep are slow and flaky. testing/synctest became generally available in Go 1.25.

synctest.Test(t, f) runs f in an isolated "bubble":

  • Time is virtual: time.Now, Sleep, timers and context deadlines all use a fake clock.
  • The fake clock advances instantly when every goroutine in the bubble is durably blocked.
  • synctest.Wait() blocks until every other goroutine in the bubble is durably blocked, which gives you a deterministic sync 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) // instant, virtual time
		synctest.Wait()
		if err := ctx.Err(); err != nil {
			t.Fatalf("expired too early: %v", err)
		}

		time.Sleep(time.Nanosecond)
		synctest.Wait()
		if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
			t.Fatal("expected deadline exceeded")
		}
	})
}

Other tools:

  • -race with -count=N.
  • goleak.VerifyNone(t) to catch leaked goroutines.
  • Injecting clocks or channels as dependencies.
  • Stress testing with -cpu=1,2,8.

Gotcha: goroutines blocked on real I/O (network sockets) or on a mutex are not "durably blocked" for synctest, so use in-memory fakes such as net.Pipe. A channel created inside the bubble must not be used from outside it; doing so panics. A goroutine blocked on a channel created outside the bubble does not count as durably blocked.

More on Concurrency Patterns & sync

All 38 Concurrency Patterns & sync questions