golang-testing-strategies

Go Testing Strategies

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-strategies" with this command: npx skills add bobmatnyc/claude-mpm-skills/bobmatnyc-claude-mpm-skills-golang-testing-strategies

Go Testing Strategies

Overview

Go provides a robust built-in testing framework (testing package) that emphasizes simplicity and developer productivity. Combined with community tools like testify and gomock, Go testing enables comprehensive test coverage with minimal boilerplate.

Key Features:

  • 📋 Table-Driven Tests: Idiomatic pattern for testing multiple inputs

  • ✅ Testify: Readable assertions and test suites

  • 🎭 Gomock: Type-safe interface mocking

  • ⚡ Benchmarking: Built-in performance testing

  • 🔍 Race Detector: Concurrent code safety verification

  • 📊 Coverage: Native coverage reporting and enforcement

  • 🚀 CI Integration: Test caching and parallel execution

When to Use This Skill

Activate this skill when:

  • Writing test suites for Go libraries or applications

  • Setting up testing infrastructure for new projects

  • Mocking external dependencies (databases, APIs, services)

  • Benchmarking performance-critical code paths

  • Ensuring thread-safe concurrent implementations

  • Integrating tests into CI/CD pipelines

  • Migrating from other testing frameworks

Core Testing Principles

The Go Testing Philosophy

  • Simplicity Over Magic: Use standard library when possible

  • Table-Driven Tests: Test multiple scenarios with single function

  • Subtests: Organize related tests with t.Run()

  • Interface-Based Mocking: Mock dependencies through interfaces

  • Test Files Colocate: Place *_test.go files alongside code

  • Package Naming: Use package_test for external tests, package for internal

Test Organization

File Naming Convention:

  • Unit tests: file_test.go

  • Integration tests: file_integration_test.go

  • Benchmark tests: Prefix with Benchmark in same test file

Package Structure:

mypackage/ ├── user.go ├── user_test.go // Internal tests (same package) ├── user_external_test.go // External tests (package mypackage_test) ├── integration_test.go // Integration tests └── testdata/ // Test fixtures (ignored by go build) └── golden.json

Table-Driven Test Pattern

Basic Structure

The idiomatic Go testing pattern for testing multiple inputs:

func TestUserValidation(t *testing.T) { tests := []struct { name string input User wantErr bool errMsg string }{ { name: "valid user", input: User{Name: "Alice", Age: 30, Email: "alice@example.com"}, wantErr: false, }, { name: "empty name", input: User{Name: "", Age: 30, Email: "alice@example.com"}, wantErr: true, errMsg: "name is required", }, { name: "invalid email", input: User{Name: "Bob", Age: 25, Email: "invalid"}, wantErr: true, errMsg: "invalid email format", }, { name: "negative age", input: User{Name: "Charlie", Age: -5, Email: "charlie@example.com"}, wantErr: true, errMsg: "age must be positive", }, }

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        err := ValidateUser(tt.input)

        if (err != nil) != tt.wantErr {
            t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
            return
        }

        if tt.wantErr && err.Error() != tt.errMsg {
            t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
        }
    })
}

}

Parallel Test Execution

Enable parallel test execution for independent tests:

func TestConcurrentOperations(t *testing.T) { tests := []struct { name string fn func() int want int }{ {"operation 1", func() int { return compute1() }, 42}, {"operation 2", func() int { return compute2() }, 84}, {"operation 3", func() int { return compute3() }, 126}, }

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

        got := tt.fn()
        if got != tt.want {
            t.Errorf("got %v, want %v", got, tt.want)
        }
    })
}

}

Testify Framework

Installation

go get github.com/stretchr/testify

Assertions

Replace verbose error checking with readable assertions:

import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" )

func TestCalculator(t *testing.T) { calc := NewCalculator()

// assert: Test continues on failure
assert.Equal(t, 5, calc.Add(2, 3))
assert.NotNil(t, calc)
assert.True(t, calc.IsReady())

// require: Test stops on failure (for critical assertions)
result, err := calc.Divide(10, 2)
require.NoError(t, err) // Stop if error occurs
assert.Equal(t, 5, result)

}

func TestUserOperations(t *testing.T) { user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}

// Object matching
assert.Equal(t, 1, user.ID)
assert.Contains(t, user.Email, "@")
assert.Len(t, user.Name, 5)

// Partial matching
assert.ObjectsAreEqual(user, &User{
    ID:    1,
    Name:  "Alice",
    Email: assert.AnythingOfType("string"),
})

}

Test Suites

Organize related tests with setup/teardown:

import ( "testing" "github.com/stretchr/testify/suite" )

type UserServiceTestSuite struct { suite.Suite db *sql.DB service *UserService }

// SetupSuite runs once before all tests func (s *UserServiceTestSuite) SetupSuite() { s.db = setupTestDatabase() s.service = NewUserService(s.db) }

// TearDownSuite runs once after all tests func (s *UserServiceTestSuite) TearDownSuite() { s.db.Close() }

// SetupTest runs before each test func (s *UserServiceTestSuite) SetupTest() { cleanDatabase(s.db) }

// TearDownTest runs after each test func (s *UserServiceTestSuite) TearDownTest() { // Cleanup if needed }

// Test methods must start with "Test" func (s *UserServiceTestSuite) TestCreateUser() { user := &User{Name: "Alice", Email: "alice@example.com"}

err := s.service.Create(user)
s.NoError(err)
s.NotEqual(0, user.ID) // ID assigned

}

func (s *UserServiceTestSuite) TestGetUser() { // Setup user := &User{Name: "Bob", Email: "bob@example.com"} s.service.Create(user)

// Test
retrieved, err := s.service.GetByID(user.ID)
s.NoError(err)
s.Equal(user.Name, retrieved.Name)

}

// Run the suite func TestUserServiceTestSuite(t *testing.T) { suite.Run(t, new(UserServiceTestSuite)) }

Gomock Interface Mocking

Installation

go install github.com/golang/mock/mockgen@latest

Generate Mocks

// user_repository.go package repository

//go:generate mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks

type UserRepository interface { GetByID(id int) (*User, error) Create(user *User) error Update(user *User) error Delete(id int) error }

Generate mocks:

go generate ./...

Or manually:

mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks

Using Mocks in Tests

import ( "testing" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "myapp/repository/mocks" )

func TestUserService_GetUser(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()

// Create mock
mockRepo := mocks.NewMockUserRepository(ctrl)

// Set expectations
expectedUser := &User{ID: 1, Name: "Alice"}
mockRepo.EXPECT().
    GetByID(1).
    Return(expectedUser, nil).
    Times(1)

// Test
service := NewUserService(mockRepo)
user, err := service.GetUser(1)

// Assertions
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)

}

func TestUserService_CreateUser_Validation(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()

mockRepo := mocks.NewMockUserRepository(ctrl)

// Expect Create to NOT be called (validation should fail first)
mockRepo.EXPECT().Create(gomock.Any()).Times(0)

service := NewUserService(mockRepo)
err := service.CreateUser(&User{Name: ""}) // Invalid user

assert.Error(t, err)
assert.Contains(t, err.Error(), "name is required")

}

Custom Matchers

// Custom matcher for complex validation type userMatcher struct { expectedEmail string }

func (m userMatcher) Matches(x interface{}) bool { user, ok := x.(*User) if !ok { return false } return user.Email == m.expectedEmail }

func (m userMatcher) String() string { return "matches user with email: " + m.expectedEmail }

func UserWithEmail(email string) gomock.Matcher { return userMatcher{expectedEmail: email} }

// Usage in test func TestCustomMatcher(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()

mockRepo := mocks.NewMockUserRepository(ctrl)

mockRepo.EXPECT().
    Create(UserWithEmail("alice@example.com")).
    Return(nil)

service := NewUserService(mockRepo)
service.CreateUser(&User{Name: "Alice", Email: "alice@example.com"})

}

Benchmark Testing

Basic Benchmarks

func BenchmarkAdd(b *testing.B) { calc := NewCalculator()

for i := 0; i < b.N; i++ {
    calc.Add(2, 3)
}

}

func BenchmarkStringConcatenation(b *testing.B) { b.Run("plus operator", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = "hello" + "world" } })

b.Run("strings.Builder", func(b *testing.B) {
    for i := 0; i &#x3C; b.N; i++ {
        var sb strings.Builder
        sb.WriteString("hello")
        sb.WriteString("world")
        _ = sb.String()
    }
})

}

Running Benchmarks

Run all benchmarks

go test -bench=.

Run specific benchmark

go test -bench=BenchmarkAdd

With memory allocation stats

go test -bench=. -benchmem

Compare benchmarks

go test -bench=. -benchmem > old.txt

Make changes

go test -bench=. -benchmem > new.txt benchstat old.txt new.txt

Benchmark Output Example

BenchmarkAdd-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op BenchmarkStringBuilder-8 50000000 28.5 ns/op 64 B/op 1 allocs/op

Reading: 50000000 iterations, 28.5 ns/op per operation, 64 B/op bytes allocated per op, 1 allocs/op allocations per op

Advanced Testing Patterns

httptest for HTTP Handlers

import ( "net/http" "net/http/httptest" "testing" )

func TestUserHandler(t *testing.T) { handler := http.HandlerFunc(UserHandler)

req := httptest.NewRequest("GET", "/users/1", nil)
rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Alice")

}

func TestHTTPClient(t *testing.T) { // Mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/api/users", r.URL.Path) w.WriteHeader(http.StatusOK) w.Write([]byte({"id": 1, "name": "Alice"})) })) defer server.Close()

// Test client against mock server
client := NewAPIClient(server.URL)
user, err := client.GetUser(1)

assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)

}

Race Detector

Detect data races in concurrent code:

go test -race ./...

Example test for concurrent safety:

func TestConcurrentMapAccess(t *testing.T) { cache := NewSafeCache()

var wg sync.WaitGroup
for i := 0; i &#x3C; 100; i++ {
    wg.Add(1)
    go func(val int) {
        defer wg.Done()
        cache.Set(fmt.Sprintf("key%d", val), val)
    }(i)
}

wg.Wait()
assert.Equal(t, 100, cache.Len())

}

Golden File Testing

Test against expected output files:

func TestRenderTemplate(t *testing.T) { output := RenderTemplate("user", User{Name: "Alice"})

goldenFile := "testdata/user_template.golden"

if *update {
    // Update golden file: go test -update
    os.WriteFile(goldenFile, []byte(output), 0644)
}

expected, err := os.ReadFile(goldenFile)
require.NoError(t, err)

assert.Equal(t, string(expected), output)

}

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

CI/CD Integration

GitHub Actions Example

.github/workflows/test.yml

name: Tests

on: [push, pull_request]

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3

  - name: Set up Go
    uses: actions/setup-go@v4
    with:
      go-version: '1.23'

  - name: Run tests
    run: go test -v -race -coverprofile=coverage.out ./...

  - name: Upload coverage
    uses: codecov/codecov-action@v3
    with:
      files: ./coverage.out

  - name: Check coverage threshold
    run: |
      go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
      awk '{if ($1 &#x3C; 80) exit 1}'

Coverage Enforcement

Generate coverage report

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

View coverage in terminal

go tool cover -func=coverage.out

Generate HTML report

go tool cover -html=coverage.out -o coverage.html

Check coverage threshold (fail if < 80%)

go test -coverprofile=coverage.out ./... &&
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'

Decision Trees

When to Use Each Testing Tool

Use Standard testing Package When:

  • Simple unit tests with few assertions

  • No external dependencies to mock

  • Performance benchmarking

  • Minimal dependencies preferred

Use Testify When:

  • Need readable assertions (assert.Equal vs verbose checks)

  • Test suites with setup/teardown

  • Multiple similar test cases

  • Prefer expressive test code

Use Gomock When:

  • Testing code with interface dependencies

  • Need precise call verification (times, order)

  • Complex mock behavior with multiple scenarios

  • Type-safe mocking required

Use Benchmarks When:

  • Optimizing performance-critical code

  • Comparing algorithm implementations

  • Detecting performance regressions

  • Memory allocation profiling

Use httptest When:

  • Testing HTTP handlers

  • Mocking external HTTP APIs

  • Integration testing HTTP clients

  • Testing middleware chains

Use Race Detector When:

  • Writing concurrent code

  • Using goroutines and channels

  • Shared state across goroutines

  • CI/CD for all concurrent code

Anti-Patterns to Avoid

❌ Don't Mock Everything

// WRONG: Over-mocking makes tests brittle mockLogger := mocks.NewMockLogger(ctrl) mockConfig := mocks.NewMockConfig(ctrl) mockMetrics := mocks.NewMockMetrics(ctrl) // Too many mocks = fragile test

✅ Do: Mock Only External Dependencies

// CORRECT: Mock only database, use real logger/config mockRepo := mocks.NewMockUserRepository(ctrl) service := NewUserService(mockRepo, realLogger, realConfig)

❌ Don't Test Implementation Details

// WRONG: Testing internal state assert.Equal(t, "processing", service.internalState)

✅ Do: Test Public Behavior

// CORRECT: Test observable outcomes user, err := service.GetUser(1) assert.NoError(t, err) assert.Equal(t, "Alice", user.Name)

❌ Don't Ignore Error Cases

// WRONG: Only testing happy path func TestGetUser(t *testing.T) { user, _ := service.GetUser(1) // Ignoring error! assert.NotNil(t, user) }

✅ Do: Test Error Conditions

// CORRECT: Test both success and error cases func TestGetUser_NotFound(t *testing.T) { user, err := service.GetUser(999) assert.Error(t, err) assert.Nil(t, user) assert.Contains(t, err.Error(), "not found") }

Best Practices

  • Colocate Tests: Place *_test.go files alongside source code

  • Use Subtests: Organize related tests with t.Run()

  • Parallel When Safe: Enable t.Parallel() for independent tests

  • Mock Interfaces: Design for testability with interface dependencies

  • Test Errors: Verify both success and failure paths

  • Benchmark Critical Paths: Profile performance-sensitive code

  • Run Race Detector: Always use -race for concurrent code

  • Enforce Coverage: Set minimum thresholds in CI (typically 80%)

  • Use Golden Files: Test complex outputs with expected files

  • Keep Tests Fast: Mock slow operations, use -short flag for quick runs

Resources

Official Documentation:

Testing Frameworks:

Recent Guides (2025):

Related Skills:

  • golang-engineer: Core Go patterns and concurrency

  • verification-before-completion: Testing as part of "done"

  • testing-anti-patterns: Avoid common testing mistakes

Quick Reference

Run Tests

go test ./... # All tests go test -v ./... # Verbose output go test -short ./... # Skip slow tests go test -run TestUserCreate # Specific test go test -race ./... # With race detector go test -cover ./... # With coverage go test -coverprofile=c.out ./... # Coverage file go test -bench=. -benchmem # Benchmarks with memory

Generate Mocks

go generate ./... # All //go:generate directives mockgen -source=interface.go -destination=mock.go

Coverage Analysis

go tool cover -func=coverage.out # Coverage per function go tool cover -html=coverage.out # HTML report

Token Estimate: ~4,500 tokens (entry point + full content) Version: 1.0.0 Last Updated: 2025-12-03

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.

General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pydantic

No summary provided by upstream source.

Repository SourceNeeds Review
General

playwright-e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwind-css

No summary provided by upstream source.

Repository SourceNeeds Review