golang-testing

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "golang-testing" with this command: npx skills add baotoq/agent-skills/baotoq-agent-skills-golang-testing

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

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 != "" && 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

design-ui-ux-pro-max

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

database-architect

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

frontend-radix-ui-design-system

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

api-design-principles

No summary provided by upstream source.

Repository SourceNeeds Review