Advanced Testing

Overview

Table-driven tests, subtests, and test helpers make Go tests maintainable and expressive.

Fast feedback test loop

unit tests -------> deterministic concurrency tests -----> benchmarks
    |                           |                             |
  <1s target                  race-safe                    track regressions

Table-Driven Tests

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -2, -3},
        {"zero", 0, 0, 0},
        {"mixed", -1, 5, 4},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

Subtests

func TestAPI(t *testing.T) {
    t.Run("GET", func(t *testing.T) {
        t.Run("success", func(t *testing.T) { })
        t.Run("not_found", func(t *testing.T) { })
    })

    t.Run("POST", func(t *testing.T) {
        t.Run("valid", func(t *testing.T) { })
        t.Run("invalid", func(t *testing.T) { })
    })
}

Test Helpers

func newTestServer(t *testing.T) *httptest.Server {
    t.Helper()
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    }))
}

func TestClient(t *testing.T) {
    server := newTestServer(t)
    defer server.Close()
    // Use server.URL
}

Cleanup

func TestWithCleanup(t *testing.T) {
    tmpDir := t.TempDir()  // Auto-removed

    f, _ := os.CreateTemp(tmpDir, "test")
    t.Cleanup(func() {
        f.Close()
    })
}

Deterministic Concurrency (testing/synctest)

For Go 1.26 codebases, adopt testing/synctest where timers, goroutines, and scheduling made tests flaky before.

import "testing/synctest"

func TestRateLimiter(t *testing.T) {
    synctest.Run(func() {
        rl := NewRateLimiter(1 * time.Second)
        // Time is virtual and controlled within synctest.Run
        if !rl.Allow() { t.Error("should allow") }
        synctest.Wait() // Wait for internal goroutines
    })
}

Tip: keep business logic outside goroutine setup so synctest.Run stays small and focused.

Efficient Benchmarks (B.Loop)

Prefer B.Loop in modern toolchains for cleaner benchmark loops and fewer loop-control mistakes.

func BenchmarkAdd(b *testing.B) {
    for b.Loop() {
        Add(1, 2)
    }
}

Migration pattern:

// old
func BenchmarkParseOld(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Parse(input)
    }
}

// new
func BenchmarkParse(b *testing.B) {
    for b.Loop() {
        Parse(input)
    }
}

Go 1.26 Testing Upgrade Checklist

  1. Use t.Cleanup, t.TempDir, and t.Setenv instead of manual teardown.
  2. Move flaky concurrency tests into testing/synctest.
  3. Standardize benchmark style on B.Loop.
  4. Run strict CI test command:
go test ./... -race -shuffle=on -count=1

Parallel Subtests

func TestParallel(t *testing.T) {
    tests := []struct{ name string }{{"a"}, {"b"}, {"c"}}

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            // Runs concurrently
        })
    }
}

Benchmarks

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}
go test -bench=.
go test -bench=. -benchmem  # Include memory

Summary

Technique Purpose
Table-driven Test multiple cases
Subtests Organize and filter
t.Helper() Clean error reporting
t.Cleanup() Guaranteed cleanup
t.Parallel() Concurrent tests