home / skills / 89jobrien / steve / golang-testing

golang-testing skill

/steve/skills/golang-testing

This skill guides comprehensive Go testing, including table-driven tests, mocks, integration tests, benchmarks, and test organization for reliable Go code.

npx playbooks add skill 89jobrien/steve --skill golang-testing

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.7 KB
---
name: golang-testing
description: Comprehensive Go testing patterns including table-driven tests, mocking,
  integration testing, benchmarks, and test organization.
author: Joseph OBrien
status: unpublished
updated: '2025-12-23'
version: 1.0.1
tag: skill
type: skill
---

# 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

```go
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

```go
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

```go
// 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

```go
// 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

```go
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

```go
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

```go
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: "[email protected]"}

        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

```go
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

```go
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

```go
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

```go
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

```text
/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
//go:build integration

package user

func TestIntegration(t *testing.T) {
    // Integration test code
}
```

Run with: `go test -tags=integration ./...`

## Coverage

```bash
# Generate coverage
go test -coverprofile=coverage.out ./...

# View in browser
go tool cover -html=coverage.out

# Check coverage percentage
go test -cover ./...
```

## Best Practices

1. **Test behavior, not implementation** - Focus on inputs and outputs
2. **One assertion per test** - Keep tests focused and clear
3. **Use t.Helper()** - Mark helper functions for better error reporting
4. **Parallel tests** - Use `t.Parallel()` for independent tests
5. **Descriptive names** - `TestUserService_CreateUser_WithInvalidEmail`
6. **Test edge cases** - Empty inputs, nil values, boundary conditions
7. **Keep tests fast** - Use mocks, skip slow tests with `-short`
8. **Avoid test pollution** - Each test should be independent

Overview

This skill delivers practical, battle-tested patterns for testing Go applications: table-driven tests, interface-based mocking, integration tests with containers, benchmarks, and test organization. It focuses on concrete examples and reusable patterns you can drop into your codebase to make tests expressive, fast, and maintainable.

How this skill works

The skill inspects common testing scenarios and provides idiomatic Go examples: table-driven unit tests with subtests, mock implementations for interfaces, Testify assertion patterns, Testcontainers-based integration tests, benchmark templates, and setup/teardown fixtures. Each pattern includes minimal, copyable code and guidance on where to apply it (unit vs integration vs performance).

When to use it

  • Writing unit tests for functions and methods where inputs/outputs vary
  • Creating table-driven tests to cover multiple cases and edge conditions
  • Mocking dependencies by implementing interfaces for isolation
  • Running integration tests that require databases or external services
  • Measuring performance with benchmarks and parallel benchmarks
  • Organizing tests, fixtures, and build tags for clarity and speed

Best practices

  • Test behavior, not implementation: assert outcomes, not internals
  • Keep tests focused: aim for one assertion intent per subtest
  • Use t.Helper() in helpers and t.Run for descriptive subtests
  • Use t.Parallel() for independent tests to speed execution
  • Skip slow integration tests with testing.Short() and use build tags
  • Avoid shared state: each test should setup and teardown its own fixtures

Example use cases

  • Table-driven tests for arithmetic, parsing, or validation logic
  • Mocking a repository or external API using small mock structs
  • Integration test that spins up a PostgreSQL container, runs migrations, and verifies repository behavior
  • Benchmarks to compare algorithm implementations and input sizes
  • HTTP handler tests using httptest with mock services for success and error flows

FAQ

When should I use Testcontainers vs mocks?

Use mocks for fast, deterministic unit tests. Use Testcontainers when you need to validate real integrations, migrations, or driver-specific behavior that mocks cannot reproduce.

How do I keep integration tests from slowing CI?

Mark them with build tags or skip them in short mode (testing.Short()). Run integration suites separately or in a dedicated pipeline stage.