Marketplace golang-testing
Comprehensive Go testing patterns including table-driven tests, mocking,
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/89jobrien/golang-testing" ~/.claude/skills/aiskillstore-marketplace-golang-testing && rm -rf "$T"
manifest:
skills/89jobrien/golang-testing/SKILL.mdsource content
Golang Testing
This skill provides guidance on comprehensive testing strategies for Go applications including unit tests, integration tests, benchmarks, and test organization.
When to Use This Skill
- When writing unit tests for Go code
- When creating table-driven tests
- When mocking dependencies with interfaces
- When writing integration tests with test containers
- When benchmarking performance-critical code
- When organizing test suites and fixtures
Table-Driven Tests
Basic Pattern
func TestAdd(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive numbers", 2, 3, 5}, {"negative numbers", -2, -3, -5}, {"mixed numbers", -2, 3, 1}, {"zeros", 0, 0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } }
With Error Cases
func TestDivide(t *testing.T) { tests := []struct { name string a, b int expected int wantErr bool errString string }{ {"valid division", 10, 2, 5, false, ""}, {"divide by zero", 10, 0, 0, true, "division by zero"}, {"negative result", -10, 2, -5, false, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Divide(tt.a, tt.b) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), tt.errString) { t.Errorf("error = %v; want containing %q", err, tt.errString) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if result != tt.expected { t.Errorf("Divide(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } }
Interface-Based Mocking
Define Interfaces
// repository.go type UserRepository interface { FindByID(ctx context.Context, id string) (*User, error) Save(ctx context.Context, user *User) error } type EmailSender interface { Send(ctx context.Context, to, subject, body string) error }
Create Mock Implementations
// mocks/user_repository.go type MockUserRepository struct { FindByIDFunc func(ctx context.Context, id string) (*User, error) SaveFunc func(ctx context.Context, user *User) error } func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) { if m.FindByIDFunc != nil { return m.FindByIDFunc(ctx, id) } return nil, nil } func (m *MockUserRepository) Save(ctx context.Context, user *User) error { if m.SaveFunc != nil { return m.SaveFunc(ctx, user) } return nil }
Use in Tests
func TestUserService_GetUser(t *testing.T) { expectedUser := &User{ID: "123", Name: "John"} repo := &MockUserRepository{ FindByIDFunc: func(ctx context.Context, id string) (*User, error) { if id == "123" { return expectedUser, nil } return nil, ErrNotFound }, } service := NewUserService(repo) t.Run("existing user", func(t *testing.T) { user, err := service.GetUser(context.Background(), "123") if err != nil { t.Fatalf("unexpected error: %v", err) } if user.Name != expectedUser.Name { t.Errorf("got name %q; want %q", user.Name, expectedUser.Name) } }) t.Run("non-existing user", func(t *testing.T) { _, err := service.GetUser(context.Background(), "456") if !errors.Is(err, ErrNotFound) { t.Errorf("got error %v; want ErrNotFound", err) } }) }
Testify Assertions
import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWithTestify(t *testing.T) { // assert continues on failure assert.Equal(t, 5, Add(2, 3), "addition should work") assert.NotNil(t, result) assert.Len(t, items, 3) assert.Contains(t, slice, item) assert.True(t, condition) assert.NoError(t, err) assert.ErrorIs(t, err, ErrNotFound) // require stops test on failure require.NoError(t, err, "setup must succeed") require.NotNil(t, config) }
Integration Tests with Testcontainers
import ( "context" "testing" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" ) func TestUserRepository_Integration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } ctx := context.Background() // Start PostgreSQL container pgContainer, err := postgres.Run(ctx, "postgres:15-alpine", postgres.WithDatabase("testdb"), postgres.WithUsername("test"), postgres.WithPassword("test"), ) require.NoError(t, err) defer pgContainer.Terminate(ctx) // Get connection string connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") require.NoError(t, err) // Connect and run migrations db, err := sql.Open("postgres", connStr) require.NoError(t, err) defer db.Close() runMigrations(db) // Create repository and test repo := NewUserRepository(db) t.Run("save and find user", func(t *testing.T) { user := &User{ID: "123", Name: "John", Email: "john@example.com"} err := repo.Save(ctx, user) require.NoError(t, err) found, err := repo.FindByID(ctx, "123") require.NoError(t, err) assert.Equal(t, user.Name, found.Name) }) }
Test Fixtures
Setup/Teardown Pattern
func TestMain(m *testing.M) { // Global setup setup() code := m.Run() // Global teardown teardown() os.Exit(code) } func setup() { // Initialize test database, load fixtures, etc. } func teardown() { // Clean up resources }
Per-Test Setup
func setupTest(t *testing.T) (*UserService, func()) { t.Helper() db := setupTestDB(t) repo := NewUserRepository(db) service := NewUserService(repo) cleanup := func() { db.Close() } return service, cleanup } func TestUserService(t *testing.T) { service, cleanup := setupTest(t) defer cleanup() // Run tests using service }
Benchmarks
func BenchmarkFibonacci(b *testing.B) { for i := 0; i < b.N; i++ { Fibonacci(20) } } func BenchmarkFibonacciParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { Fibonacci(20) } }) } // With sub-benchmarks func BenchmarkSort(b *testing.B) { sizes := []int{100, 1000, 10000} for _, size := range sizes { b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) { data := generateData(size) b.ResetTimer() for i := 0; i < b.N; i++ { sort.Ints(data) } }) } }
Testing HTTP Handlers
func TestHandler_GetUser(t *testing.T) { // Setup mock service service := &MockUserService{ GetUserFunc: func(ctx context.Context, id string) (*User, error) { return &User{ID: id, Name: "John"}, nil }, } handler := NewHandler(service) t.Run("success", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/users/123", nil) rec := httptest.NewRecorder() handler.GetUser(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var response User err := json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.Equal(t, "John", response.Name) }) t.Run("not found", func(t *testing.T) { service.GetUserFunc = func(ctx context.Context, id string) (*User, error) { return nil, ErrNotFound } req := httptest.NewRequest(http.MethodGet, "/users/999", nil) rec := httptest.NewRecorder() handler.GetUser(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) }) }
Test Organization
File Structure
/internal /user user.go user_test.go # Unit tests user_integration_test.go # Integration tests (build tag) testdata/ # Test fixtures users.json
Build Tags for Integration Tests
//go:build integration package user func TestIntegration(t *testing.T) { // Integration test code }
Run with:
go test -tags=integration ./...
Coverage
# Generate coverage go test -coverprofile=coverage.out ./... # View in browser go tool cover -html=coverage.out # Check coverage percentage go test -cover ./...
Best Practices
- Test behavior, not implementation - Focus on inputs and outputs
- One assertion per test - Keep tests focused and clear
- Use t.Helper() - Mark helper functions for better error reporting
- Parallel tests - Use
for independent testst.Parallel() - Descriptive names -
TestUserService_CreateUser_WithInvalidEmail - Test edge cases - Empty inputs, nil values, boundary conditions
- Keep tests fast - Use mocks, skip slow tests with
-short - Avoid test pollution - Each test should be independent