# Golang Testing Strategies

> Comprehensive Go testing strategies including table-driven tests, testify assertions, gomock interface mocking, benchmark testing, and CI/CD integration

- **Type:** Skill
- **Install:** `agentstack add skill-bobmatnyc-claude-mpm-skills-golang-testing-strategies`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [bobmatnyc](https://agentstack.voostack.com/s/bobmatnyc)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [bobmatnyc](https://github.com/bobmatnyc)
- **Source:** https://github.com/bobmatnyc/claude-mpm-skills/tree/main/toolchains/golang/golang-testing-strategies

## Install

```sh
agentstack add skill-bobmatnyc-claude-mpm-skills-golang-testing-strategies
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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

1. **Simplicity Over Magic**: Use standard library when possible
2. **Table-Driven Tests**: Test multiple scenarios with single function
3. **Subtests**: Organize related tests with `t.Run()`
4. **Interface-Based Mocking**: Mock dependencies through interfaces
5. **Test Files Colocate**: Place `*_test.go` files alongside code
6. **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:

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

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

```bash
go get github.com/stretchr/testify
```

### Assertions

Replace verbose error checking with readable assertions:

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

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

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

### Generate Mocks

```go
// 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:
```bash
go generate ./...
# Or manually:
mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
```

### Using Mocks in Tests

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

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

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

    for i := 0; i  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

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

```bash
go test -race ./...
```

Example test for concurrent safety:

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

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

```yaml
# .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

```bash
# 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**
```go
// 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**
```go
// CORRECT: Mock only database, use real logger/config
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo, realLogger, realConfig)
```

❌ **Don't Test Implementation Details**
```go
// WRONG: Testing internal state
assert.Equal(t, "processing", service.internalState)
```

✅ **Do: Test Public Behavior**
```go
// CORRECT: Test observable outcomes
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
```

❌ **Don't Ignore Error Cases**
```go
// WRONG: Only testing happy path
func TestGetUser(t *testing.T) {
    user, _ := service.GetUser(1) // Ignoring error!
    assert.NotNil(t, user)
}
```

✅ **Do: Test Error Conditions**
```go
// 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

1. **Colocate Tests**: Place `*_test.go` files alongside source code
2. **Use Subtests**: Organize related tests with `t.Run()`
3. **Parallel When Safe**: Enable `t.Parallel()` for independent tests
4. **Mock Interfaces**: Design for testability with interface dependencies
5. **Test Errors**: Verify both success and failure paths
6. **Benchmark Critical Paths**: Profile performance-sensitive code
7. **Run Race Detector**: Always use `-race` for concurrent code
8. **Enforce Coverage**: Set minimum thresholds in CI (typically 80%)
9. **Use Golden Fi

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [bobmatnyc](https://github.com/bobmatnyc)
- **Source:** [bobmatnyc/claude-mpm-skills](https://github.com/bobmatnyc/claude-mpm-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-bobmatnyc-claude-mpm-skills-golang-testing-strategies
- Seller: https://agentstack.voostack.com/s/bobmatnyc
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
