golang-testing

Table-driven tests, subtests, benchmarks, fuzzing, and golden files for Go TDD workflows. Covers the RED-GREEN-REFACTOR cycle with step-by-step examples for writing tests before implementation Includes table-driven test patterns for comprehensive coverage, error cases, and parallel execution with t.Run() Provides benchmarking techniques for performance analysis, memory allocation tracking, and comparative benchmarks across input sizes Supports fuzzing with seed corpus and property-based testing for input validation (Go 1.18+) Includes mocking via interfaces, HTTP handler testing with httptest , golden file comparisons, and coverage measurement strategies

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill golang-testing
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Go Testing Patterns

Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology.

When to Activate

  • Writing new Go functions or methods
  • Adding test coverage to existing code
  • Creating benchmarks for performance-critical code
  • Implementing fuzz tests for input validation
  • Following TDD workflow in Go projects

TDD Workflow for Go

The RED-GREEN-REFACTOR Cycle

RED     → Write a failing test first

GREEN   → Write minimal code to pass the test

REFACTOR → Improve code while keeping tests green

REPEAT  → Continue with next requirement

Step-by-Step TDD in Go

// Step 1: Define the interface/signature

// calculator.go

package calculator

func Add(a, b int) int {

    panic("not implemented") // Placeholder

}

// Step 2: Write failing test (RED)

// calculator_test.go

package calculator

import "testing"

func TestAdd(t *testing.T) {

    got := Add(2, 3)

    want := 5

    if got != want {

        t.Errorf("Add(2, 3) = %d; want %d", got, want)

    }

}

// Step 3: Run test - verify FAIL

// $ go test

// --- FAIL: TestAdd (0.00s)

// panic: not implemented

// Step 4: Implement minimal code (GREEN)

func Add(a, b int) int {

    return a + b

}

// Step 5: Run test - verify PASS

// $ go test

// PASS

// Step 6: Refactor if needed, verify tests still pass

Table-Driven Tests

The standard pattern for Go tests. Enables comprehensive coverage with minimal code.

func TestAdd(t *testing.T) {

    tests := []struct {

        name     string

        a, b     int

        expected int

    }{

        {"positive numbers", 2, 3, 5},

        {"negative numbers", -1, -2, -3},

        {"zero values", 0, 0, 0},

        {"mixed signs", -1, 1, 0},

        {"large numbers", 1000000, 2000000, 3000000},

    }

    for _, tt := range tests {

        t.Run(tt.name, func(t *testing.T) {

            got := Add(tt.a, tt.b)

            if got != tt.expected {

                t.Errorf("Add(%d, %d) = %d; want %d",

                    tt.a, tt.b, got, tt.expected)

            }

        })

    }

}

Table-Driven Tests with Error Cases

func TestParseConfig(t *testing.T) {

    tests := []struct {

        name    string

        input   string

        want    *Config

        wantErr bool

    }{

        {

            name:  "valid config",

            input: `{"host": "localhost", "port": 8080}`,

            want:  &Config{Host: "localhost", Port: 8080},

        },

        {

            name:    "invalid JSON",

            input:   `{invalid}`,

            wantErr: true,

        },

        {

            name:    "empty input",

            input:   "",

            wantErr: true,

        },

        {

            name:  "minimal config",

            input: `{}`,

            want:  &Config{}, // Zero value config

        },

    }

    for _, tt := range tests {

        t.Run(tt.name, func(t *testing.T) {

            got, err := ParseConfig(tt.input)

            if tt.wantErr {

                if err == nil {

                    t.Error("expected error, got nil")

                }

                return

            }

            if err != nil {

                t.Fatalf("unexpected error: %v", err)

            }

            if !reflect.DeepEqual(got, tt.want) {

                t.Errorf("got %+v; want %+v", got, tt.want)

            }

        })

    }

}

Subtests and Sub-benchmarks

Organizing Related Tests

func TestUser(t *testing.T) {

    // Setup shared by all subtests

    db := setupTestDB(t)

    t.Run("Create", func(t *testing.T) {

        user := &User{Name: "Alice"}

        err := db.CreateUser(user)

        if err != nil {

            t.Fatalf("CreateUser failed: %v", err)

        }

        if user.ID == "" {

            t.Error("expected user ID to be set")

        }

    })

    t.Run("Get", func(t *testing.T) {

        user, err := db.GetUser("alice-id")

        if err != nil {

            t.Fatalf("GetUser failed: %v", err)

        }

        if user.Name != "Alice" {

            t.Errorf("got name %q; want %q", user.Name, "Alice")

        }

    })

    t.Run("Update", func(t *testing.T) {

        // ...

    })

    t.Run("Delete", func(t *testing.T) {

        // ...

    })

}

Parallel Subtests

func TestParallel(t *testing.T) {

    tests := []struct {

        name  string

        input string

    }{

        {"case1", "input1"},

        {"case2", "input2"},

        {"case3", "input3"},

    }

    for _, tt := range tests {

        tt := tt // Capture range variable

        t.Run(tt.name, func(t *testing.T) {

            t.Parallel() // Run subtests in parallel

            result := Process(tt.input)

            // assertions...

            _ = result

        })

    }

}

Test Helpers

Helper Functions

func setupTestDB(t *testing.T) *sql.DB {

    t.Helper() // Marks this as a helper function

    db, err := sql.Open("sqlite3", ":memory:")

    if err != nil {

        t.Fatalf("failed to open database: %v", err)

    }

    // Cleanup when test finishes

    t.Cleanup(func() {

        db.Close()

    })

    // Run migrations

    if _, err := db.Exec(schema); err != nil {

        t.Fatalf("failed to create schema: %v", err)

    }

    return db

}

func assertNoError(t *testing.T, err error) {

    t.Helper()

    if err != nil {

        t.Fatalf("unexpected error: %v", err)

    }

}

func assertEqual[T comparable](t *testing.T, got, want T) {

    t.Helper()

    if got != want {

        t.Errorf("got %v; want %v", got, want)

    }

}

Temporary Files and Directories

func TestFileProcessing(t *testing.T) {

    // Create temp directory - automatically cleaned up

    tmpDir := t.TempDir()

    // Create test file

    testFile := filepath.Join(tmpDir, "test.txt")

    err := os.WriteFile(testFile, []byte("test content"), 0644)

    if err != nil {

        t.Fatalf("failed to create test file: %v", err)

    }

    // Run test

    result, err := ProcessFile(testFile)

    if err != nil {

        t.Fatalf("ProcessFile failed: %v", err)

    }

    // Assert...

    _ = result

}

Golden Files

Testing against expected output files stored in testdata/.

var update = flag.Bool("update", false, "update golden files")

func TestRender(t *testing.T) {

    tests := []struct {

        name  string

        input Template

    }{

        {"simple", Template{Name: "test"}},

        {"complex", Template{Name: "test", Items: []string{"a", "b"}}},

    }

    for _, tt := range tests {

        t.Run(tt.name, func(t *testing.T) {

            got := Render(tt.input)

            golden := filepath.Join("testdata", tt.name+".golden")

            if *update {

                // Update golden file: go test -update

                err := os.WriteFile(golden, got, 0644)

                if err != nil {

                    t.Fatalf("failed to update golden file: %v", err)

                }

            }

            want, err := os.ReadFile(golden)

            if err != nil {

                t.Fatalf("failed to read golden file: %v", err)

            }

            if !bytes.Equal(got, want) {

                t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)

            }

        })

    }

}

Mocking with Interfaces

Interface-Based Mocking

// Define interface for dependencies

type UserRepository interface {

    GetUser(id string) (*User, error)

    SaveUser(user *User) error

}

// Production implementation

type PostgresUserRepository struct {

    db *sql.DB

}

func (r *PostgresUserRepository) GetUser(id string) (*User, error) {

    // Real database query

}

// Mock implementation for tests

type MockUserRepository struct {

    GetUserFunc  func(id string) (*User, error)

    SaveUserFunc func(user *User) error

}

func (m *MockUserRepository) GetUser(id string) (*User, error) {

    return m.GetUserFunc(id)

}

func (m *MockUserRepository) SaveUser(user *User) error {

    return m.SaveUserFunc(user)

}

// Test using mock

func TestUserService(t *testing.T) {

    mock := &MockUserRepository{

        GetUserFunc: func(id string) (*User, error) {

            if id == "123" {

                return &User{ID: "123", Name: "Alice"}, nil

            }

            return nil, ErrNotFound

        },

    }

    service := NewUserService(mock)

    user, err := service.GetUserProfile("123")

    if err != nil {

        t.Fatalf("unexpected error: %v", err)

    }

    if user.Name != "Alice" {

        t.Errorf("got name %q; want %q", user.Name, "Alice")

    }

}

Benchmarks

Basic Benchmarks

func BenchmarkProcess(b *testing.B) {

    data := generateTestData(1000)

    b.ResetTimer() // Don't count setup time

    for i := 0; i < b.N; i++ {

        Process(data)

    }

}

// Run: go test -bench=BenchmarkProcess -benchmem

// Output: BenchmarkProcess-8   10000   105234 ns/op   4096 B/op   10 allocs/op

Benchmark with Different Sizes

func BenchmarkSort(b *testing.B) {

    sizes := []int{100, 1000, 10000, 100000}

    for _, size := range sizes {

        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {

            data := generateRandomSlice(size)

            b.ResetTimer()

            for i := 0; i < b.N; i++ {

                // Make a copy to avoid sorting already sorted data

                tmp := make([]int, len(data))

                copy(tmp, data)

                sort.Ints(tmp)

            }

        })

    }

}

Memory Allocation Benchmarks

func BenchmarkStringConcat(b *testing.B) {

    parts := []string{"hello", "world", "foo", "bar", "baz"}

    b.Run("plus", func(b *testing.B) {

        for i := 0; i < b.N; i++ {

            var s string

            for _, p := range parts {

                s += p

            }

            _ = s

        }

    })

    b.Run("builder", func(b *testing.B) {

        for i := 0; i < b.N; i++ {

            var sb strings.Builder

            for _, p := range parts {

                sb.WriteString(p)

            }

            _ = sb.String()

        }

    })

    b.Run("join", func(b *testing.B) {

        for i := 0; i < b.N; i++ {

            _ = strings.Join(parts, "")

        }

    })

}

Fuzzing (Go 1.18+)

Basic Fuzz Test

func FuzzParseJSON(f *testing.F) {

    // Add seed corpus

    f.Add(`{"name": "test"}`)

    f.Add(`{"count": 123}`)

    f.Add(`[]`)

    f.Add(`""`)

    f.Fuzz(func(t *testing.T, input string) {

        var result map[string]interface{}

        err := json.Unmarshal([]byte(input), &#x26;result)

        if err != nil {

            // Invalid JSON is expected for random input

            return

        }

        // If parsing succeeded, re-encoding should work

        _, err = json.Marshal(result)

        if err != nil {

            t.Errorf("Marshal failed after successful Unmarshal: %v", err)

        }

    })

}

// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s

Fuzz Test with Multiple Inputs

func FuzzCompare(f *testing.F) {

    f.Add("hello", "world")

    f.Add("", "")

    f.Add("abc", "abc")

    f.Fuzz(func(t *testing.T, a, b string) {

        result := Compare(a, b)

        // Property: Compare(a, a) should always equal 0

        if a == b &#x26;&#x26; result != 0 {

            t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)

        }

        // Property: Compare(a, b) and Compare(b, a) should have opposite signs

        reverse := Compare(b, a)

        if (result > 0 &#x26;&#x26; reverse >= 0) || (result < 0 &#x26;&#x26; reverse <= 0) {

            if result != 0 || reverse != 0 {

                t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",

                    a, b, result, b, a, reverse)

            }

        }

    })

}

Test Coverage

Running Coverage

# Basic coverage

go test -cover ./...

# Generate coverage profile

go test -coverprofile=coverage.out ./...

# View coverage in browser

go tool cover -html=coverage.out

# View coverage by function

go tool cover -func=coverage.out

# Coverage with race detection

go test -race -coverprofile=coverage.out ./...

Coverage Targets

Code Type

Target

Critical business logic

100%

Public APIs

90%+

General code

80%+

Generated code

Exclude

Excluding Generated Code from Coverage

//go:generate mockgen -source=interface.go -destination=mock_interface.go

// In coverage profile, exclude with build tags:

// go test -cover -tags=!generate ./...

HTTP Handler Testing

func TestHealthHandler(t *testing.T) {

    // Create request

    req := httptest.NewRequest(http.MethodGet, "/health", nil)

    w := httptest.NewRecorder()

    // Call handler

    HealthHandler(w, req)

    // Check response

    resp := w.Result()

    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {

        t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)

    }

    body, _ := io.ReadAll(resp.Body)

    if string(body) != "OK" {

        t.Errorf("got body %q; want %q", body, "OK")

    }

}

func TestAPIHandler(t *testing.T) {

    tests := []struct {

        name       string

        method     string

        path       string

        body       string

        wantStatus int

        wantBody   string

    }{

        {

            name:       "get user",

            method:     http.MethodGet,

            path:       "/users/123",

            wantStatus: http.StatusOK,

            wantBody:   `{"id":"123","name":"Alice"}`,

        },

        {

            name:       "not found",

            method:     http.MethodGet,

            path:       "/users/999",

            wantStatus: http.StatusNotFound,

        },

        {

            name:       "create user",

            method:     http.MethodPost,

            path:       "/users",

            body:       `{"name":"Bob"}`,

            wantStatus: http.StatusCreated,

        },

    }

    handler := NewAPIHandler()

    for _, tt := range tests {

        t.Run(tt.name, func(t *testing.T) {

            var body io.Reader

            if tt.body != "" {

                body = strings.NewReader(tt.body)

            }

            req := httptest.NewRequest(tt.method, tt.path, body)

            req.Header.Set("Content-Type", "application/json")

            w := httptest.NewRecorder()

            handler.ServeHTTP(w, req)

            if w.Code != tt.wantStatus {

                t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)

            }

            if tt.wantBody != "" &#x26;&#x26; w.Body.String() != tt.wantBody {

                t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)

            }

        })

    }

}

Testing Commands

# Run all tests

go test ./...

# Run tests with verbose output

go test -v ./...

# Run specific test

go test -run TestAdd ./...

# Run tests matching pattern

go test -run "TestUser/Create" ./...

# Run tests with race detector

go test -race ./...

# Run tests with coverage

go test -cover -coverprofile=coverage.out ./...

# Run short tests only

go test -short ./...

# Run tests with timeout

go test -timeout 30s ./...

# Run benchmarks

go test -bench=. -benchmem ./...

# Run fuzzing

go test -fuzz=FuzzParse -fuzztime=30s ./...

# Count test runs (for flaky test detection)

go test -count=10 ./...

Best Practices

DO:

  • Write tests FIRST (TDD)
  • Use table-driven tests for comprehensive coverage
  • Test behavior, not implementation
  • Use t.Helper() in helper functions
  • Use t.Parallel() for independent tests
  • Clean up resources with t.Cleanup()
  • Use meaningful test names that describe the scenario

DON'T:

  • Test private functions directly (test through public API)
  • Use time.Sleep() in tests (use channels or conditions)
  • Ignore flaky tests (fix or remove them)
  • Mock everything (prefer integration tests when possible)
  • Skip error path testing

Integration with CI/CD

# GitHub Actions example

test:

  runs-on: ubuntu-latest

  steps:

    - uses: actions/checkout@v4

    - uses: actions/setup-go@v5

      with:

        go-version: '1.22'

    - name: Run tests

      run: go test -race -coverprofile=coverage.out ./...

    - name: Check coverage

      run: |

        go tool cover -func=coverage.out | grep total | awk '{print $3}' | \

        awk -F'%' '{if ($1 < 80) exit 1}'

Remember: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card