Skip to content
golang-testing-strategies logo

Go Testing Strategies

golang-testing-strategies

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

mattnigh/skills_collection0installs24stars

SKILL.md

Full skill instructions

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:

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: "[email protected]"},
            wantErr: false,
        },
        {
            name:    "empty name",
            input:   User{Name: "", Age: 30, Email: "[email protected]"},
            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: "[email protected]"},
            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: "[email protected]"}

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

    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: "[email protected]"}
    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("[email protected]")).
        Return(nil)

    service := NewUserService(mockRepo)
    service.CreateUser(&User{Name: "Alice", Email: "[email protected]"})
}

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

  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 Files: Test complex outputs with expected files
  10. Keep Tests Fast: Mock slow operations, use -short flag for quick runs

Resources

Official Documentation:

Testing Frameworks:

Recent Guides (2025):

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

More skills from mattnigh

spring-framework-patterns logo
mattnigh/skills_collection

spring-framework-patterns

Comprehensive Spring Framework and Spring Boot best practices including dependency injection patterns, bean lifecycle and scopes, REST API development, Spring Data JPA, service layer design, Spring Security, testing strategies, caching, AOP, async processing, error handling, and common anti-patte...

24 0
View

TypeScript厳格モードによる型安全性設計を専門とするスキル。 📖 参照書籍: - 『Effective TypeScript』(Dan Vanderkam): 型設計 📚 リソース参照: - `resources/Level1_basics.md`: レベル1の基礎ガイド - `resources/Level2_intermediate.md`: レベル2の実務ガイド - `resources/Level3_advanced.md`: レベル3の応用ガイド - `resources/Level4_expert.md`: レベル4の専門ガイド - `resources/di...

24 0
View
deploying-cloud-k8s logo
mattnigh/skills_collection

deploying-cloud-k8s

Deploys applications to cloud Kubernetes (AKS/GKE/DOKS) with CI/CD pipelines. Use when deploying to production, setting up GitHub Actions, troubleshooting deployments. Covers build-time vs runtime vars, architecture matching, and battle-tested debugging.

24 0
View
moai-alfred-agent-guide logo
mattnigh/skills_collection

moai-alfred-agent-guide

19-agent team structure, decision trees for agent selection, Haiku vs Sonnet model selection, and agent collaboration principles. Use when deciding which sub-agent to invoke, understanding team responsibilities, or learning multi-agent orchestration.

24 0
View
mermaid-diagram-generator logo
mattnigh/skills_collection

mermaid-diagram-generator

Converts architecture descriptions, module specs, or workflow docs into Mermaid diagrams. Use when visualizing brick module relationships, workflows (DDD, investigation), or system architecture. Supports: flowcharts, sequence diagrams, class diagrams, state machines, entity relationship diagrams,...

24 0
View
pre-flight-check logo
mattnigh/skills_collection

pre-flight-check

INVOKE FIRST before any code work. Validates git workflow (branch, issue, worklog) and checks approach. Use at START of every task and END before completing. Prevents skipped steps.

24 0
View
browser-dev-tools logo
mattnigh/skills_collection

browser-dev-tools

This skill should be used when working with browser-rendered artifacts (i.e. Bun, React/React Native) to proactively validate that development work on localhost is being built appropriately and to support debugging browser-rendered content. Use this skill after making frontend changes to verify t...

24 0
View
lead-dev logo
mattnigh/skills_collection

lead-dev

Lead Développeur - Coordination technique opérationnelle, code review, mentoring et livraison. Pair de web-dev-process au niveau OPÉRATIONS.

24 0
View
worker-monitor logo
mattnigh/skills_collection

worker-monitor

Monitor Docker workers, RQ queue health, and auto-scale workers (max 4). Use when: checking job progress, monitoring queue depth, scaling workers up/down, diagnosing slow processing, or waiting for jobs to complete. Referenced by data-quality skill during reprocessing.

24 0
View
quality-advisor logo
mattnigh/skills_collection

quality-advisor

Proactive quality guidance system that monitors artifact creation and provides real-time feedback on documentation quality

24 0
View
bump-version logo
mattnigh/skills_collection

bump-version

This skill should be used when the user wants to bump the version number in the workspace. It updates versions across all pyproject.toml files (root, tde, and tda packages) and the CHANGELOG.md to keep them synchronized.

24 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View