golang-testing-strategiesโ
bobmatnyc/claude-mpm-skills ยท updated Apr 8, 2026
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.
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.gofiles alongside code - Package Naming: Use
package_testfor external tests,packagefor internal
Test Organization
File Naming Convention:
- Unit tests:
file_test.go - Integration tests:
file_integration_test.go - Benchmark tests: Prefix with
Benchmarkin 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.Equalvs 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.gofiles 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
-racefor 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
-shortflag 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