golang-testing-strategies

安装量: 167
排名: #5154

安装

npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill 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 < 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 < 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 < 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:

Go Testing Package: https://pkg.go.dev/testing Table-Driven Tests: https://github.com/golang/go/wiki/TableDrivenTests Subtests and Sub-benchmarks: https://go.dev/blog/subtests

Testing Frameworks:

Testify: https://github.com/stretchr/testify Gomock: https://github.com/golang/mock httptest: https://pkg.go.dev/net/http/httptest

Recent Guides (2025):

"Go Unit Testing: Structure & Best Practices" (November 2025) Go Wiki: CommonMistakes in Testing Google Go Style Guide - Testing: https://google.github.io/styleguide/go/

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

返回排行榜