# Testing Concurrent Code — Go Concurrency Patterns

Source: https://www.skillbyai.com/en/go-concurrency/gc-testing

> Deterministic tests instead of sleeps and luck.

## Control time and scheduling

Flaky concurrent tests usually rely on `time.Sleep` to "wait long enough". Better approaches: make concurrency **injectable** (pass channels, clocks or a worker count so a test can use 1 worker or a fake clock); **wait on explicit signals** (channels, WaitGroups) instead of sleeping; give every blocking wait in a test a timeout so a bug fails the test rather than hanging it; run tests with `-race` and with `-count` greater than 1 to shake out ordering bugs. Go 1.25 added the `testing/synctest` package (experimental in Go 1.24 behind `GOEXPERIMENT=synctest`): `synctest.Test(t, func(t *testing.T) {...})` runs the function in an isolated **bubble** with a fake clock that advances only when every goroutine in the bubble is blocked, and `synctest.Wait()` waits until they all are. Timers and timeouts then complete instantly and deterministically. The API changed between versions, so check the docs for yours.

## Testing a timeout with synctest (Go 1.25+)

The 5-second timeout completes without the test taking 5 real seconds.

```go
package fetch

import (
	"context"
	"errors"
	"testing"
	"testing/synctest"
	"time"
)

func waitForResult(ctx context.Context, ch <-chan string) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	select {
	case v := <-ch:
		return v, nil
	case <-ctx.Done():
		return "", ctx.Err()
	}
}

func TestWaitForResultTimesOut(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		never := make(chan string) // nothing is ever sent
		start := time.Now()        // fake clock inside the bubble
		_, err := waitForResult(context.Background(), never)
		if !errors.Is(err, context.DeadlineExceeded) {
			t.Fatalf("want DeadlineExceeded, got %v", err)
		}
		if got := time.Since(start); got != 5*time.Second {
			t.Fatalf("fake time elapsed = %v", got)
		}
	})
}

// Shell: go test -race -count=20 ./...
```

## Test the stop path too

For every component that starts goroutines, write a test that cancels it and then checks, with goleak or a WaitGroup, that every goroutine has exited. Shutdown bugs hide on paths nobody tests.

**Quiz:** Inside a testing/synctest bubble, when does the fake clock advance?

- [ ] Only when the test calls time.Sleep in the main goroutine
- [ ] Every real millisecond, like the normal clock
- [x] When every goroutine in the bubble is durably blocked
- [ ] Never; timers inside a bubble are disabled

*Answer:* When every goroutine in the bubble is durably blocked. Time jumps forward to the next timer once nothing in the bubble can make progress, making timeouts deterministic.
