Go

How do you test context-aware code, including timeouts and cancellation?

Question 287MediumGo 1.22 to 1.25

Techniques:

  • t.Context() (Go 1.24): a per-test context that is canceled just before t.Cleanup functions run. It replaces context.Background() in tests, and goroutines started by the test see the cancellation when the test ends.
  • Pre-canceled contexts: check that a function returns immediately and cleanly.
  • Short deadlines combined with blocking fakes: check that code gives up and doesn't leak.
  • testing/synctest (Go 1.25): fake time inside a bubble, so tests of a WithTimeout(ctx, time.Hour) path run instantly and deterministically.
  • go.uber.org/goleak to assert that no goroutines are left behind after cancellation.
func TestFetchHonorsCancel(t *testing.T) {
	block := make(chan struct{})
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		select {
		case <-r.Context().Done(): // server sees the client abort
		case <-block:
		}
	}))
	defer srv.Close()
	defer close(block)

	ctx, cancel := context.WithTimeout(t.Context(), 20*time.Millisecond)
	defer cancel()

	_, err := fetch(ctx, srv.URL)
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("want DeadlineExceeded, got %v", err)
	}
}

Gotcha: assert on the error with errors.Is, not by comparing strings. Avoid time.Sleep-based synchronization; use channels, or synctest.Wait.

More on Context

All 35 Context questions