Golang Testing

affaan-m/ECC/pi/core/skills/golang-testing

by affaan-mef648e01899ba3e8dc6371642deaaf64b4477775No license275K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 4 days ago

Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices. Use when writing Go tests — table-driven cases, subtests, benchmarks, fuzzing, or coverage.

Instructions onlySoftware Development
AI-generated overview

Guidance on idiomatic Go testing: table-driven tests, subtests, benchmarks, fuzzing, mocks and coverage.

What it does
This skill provides reference patterns and code examples for writing Go tests, covering table-driven tests, subtests and parallel subtests, test helpers, golden files, interface-based mocking, benchmarks, fuzz tests and coverage measurement. It also outlines a RED-GREEN-REFACTOR TDD workflow and lists go test commands and CI coverage checks. It produces guidance and example code rather than running anything itself.
When to use it
Use it when writing or extending Go tests, adding coverage to existing Go code, creating benchmarks for performance-critical paths, or implementing fuzz tests for input validation. It also fits projects following TDD in Go.
Requirements
Requires a Go toolchain to run the described go test commands; no scripts or credentials are included, and the skill is instructions only.

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 firstGREEN   → Write minimal code to pass the testREFACTOR → Improve code while keeping tests greenREPEAT  → Continue with next requirement

Step-by-Step TDD in Go

go
// Step 1: Define the interface/signature// calculator.gopackage calculator
func Add(a, b int) int {    panic("not implemented") // Placeholder}
// Step 2: Write failing test (RED)// calculator_test.gopackage 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.

go
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

go
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

go
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

go
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

go
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

go
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/.

go
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

go
// Define interface for dependenciestype UserRepository interface {    GetUser(id string) (*User, error)    SaveUser(user *User) error}
// Production implementationtype PostgresUserRepository struct {    db *sql.DB}
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {    // Real database query}
// Mock implementation for teststype 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 mockfunc 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

go
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

go
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

go
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

go
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), &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

go
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 && 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 && reverse >= 0) || (result < 0 && 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

bash
# Basic coveragego test -cover ./...
# Generate coverage profilego test -coverprofile=coverage.out ./...
# View coverage in browsergo tool cover -html=coverage.out
# View coverage by functiongo tool cover -func=coverage.out
# Coverage with race detectiongo test -race -coverprofile=coverage.out ./...

Coverage Targets

Code TypeTarget
Critical business logic100%
Public APIs90%+
General code80%+
Generated codeExclude

Excluding Generated Code from Coverage

go
//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

go
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 != "" && w.Body.String() != tt.wantBody {                t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)            }        })    }}

Testing Commands

bash
# Run all testsgo test ./...
# Run tests with verbose outputgo test -v ./...
# Run specific testgo test -run TestAdd ./...
# Run tests matching patterngo test -run "TestUser/Create" ./...
# Run tests with race detectorgo test -race ./...
# Run tests with coveragego test -cover -coverprofile=coverage.out ./...
# Run short tests onlygo test -short ./...
# Run tests with timeoutgo test -timeout 30s ./...
# Run benchmarksgo test -bench=. -benchmem ./...
# Run fuzzinggo 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

yaml
# GitHub Actions exampletest:  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.

Source and attribution

Source:affaan-m/ECCinpi/core/skills/golang-testingat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal