Skip to content

Chapter 26: Parallel Tests

Description

Test concurrent access to shared data with t.Parallel(), sync.Mutex, and sync/atomic. Unsafe operations (plain int increment, slice append without lock) produce data races; safe variants using mutexes or atomics prevent them. Run tests with go test -race to detect unsynchronized access.

Code

type UnsafeCounter struct { value int }
func (c *UnsafeCounter) Increment() { c.value++ }

type SafeCounter struct {
    mu    sync.Mutex
    value int
}
func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

type AtomicCounter struct {
    value atomic.Int64
}
func (c *AtomicCounter) Increment() { c.value.Add(1) }

Test

func TestUnsafeCounter_ParallelRace(t *testing.T) {
    c := &UnsafeCounter{}
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Increment()
        }()
    }
    wg.Wait()
    // Run with -race: detects data race on c.value
    t.Logf("final value: %d (expected 1000)", c.Value())
}

func TestSafeCounter_Parallel(t *testing.T) {
    c := &SafeCounter{}
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Increment()
        }()
    }
    wg.Wait()
    assert.Equal(t, 1000, c.Value())
}

func TestAtomicCounter_Parallel(t *testing.T) {
    c := &AtomicCounter{}
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Increment()
        }()
    }
    wg.Wait()
    assert.Equal(t, int64(1000), c.Value())
}

func TestParallelSubtests(t *testing.T) {
    values := []int{1, 2, 3, 4, 5}
    for _, v := range values {
        v := v
        t.Run("", func(t *testing.T) {
            t.Parallel()
            assert.Greater(t, v, 0)
        })
    }
}

Testing Approach

Parallel test patterns:

  1. Race detectionTestUnsafeCounter_ParallelRace demonstrates a data race. Running with go test -race reports the race. The SafeCounter and AtomicCounter tests are race-free.
  2. wg.Wait() for goroutine completion — all parallel tests use sync.WaitGroup to ensure goroutines finish before assertions. Without it, some goroutines may not have incremented yet.
  3. t.Parallel() for subtestsTestParallelSubtests runs each subtest in parallel with t.Parallel(). The v := v copy captures the loop variable per-iteration, preventing the closure-iteration bug.
  4. sync/atomic for single-value contentionAtomicCounter uses atomic.Int64.Add(1) which is lock-free and faster than a mutex for simple counters. Use sync.Mutex when protecting multi-field structs or compound operations.

View source code on GitHub