Skill
Community

testing-patterns

Install
1
Install the plugin
$
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-golang

Want just this skill?

Then install: npx claudepluginhub u/[userId]/[slug]

Description

This skill should be used when writing Go tests, creating test fixtures, benchmarks, table-driven tests, mocking dependencies, or improving test coverage.

Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

Go Testing Patterns and Best Practices

This skill defines comprehensive testing patterns for Go, covering unit tests, integration tests, benchmarks, mocking, and test organization.

Table-Driven Tests

Default Testing Pattern

Table-driven tests are the standard pattern for Go testing. They reduce duplication and make it easy to add new test cases.

// CORRECT: Table-driven test pattern
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {name: "positive numbers", a: 2, b: 3, expected: 5},
        {name: "negative numbers", a: -2, b: -3, expected: -5},
        {name: "mixed signs", a: -2, b: 3, expected: 1},
        {name: "zero values", a: 0, b: 0, expected: 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.expected {
                t.Errorf("Add(%d, %d) = %d, expected %d", tt.a, tt.b, got, tt.expected)
            }
        })
    }
}
// WRONG: Repetitive individual tests
func TestAddPositive(t *testing.T) {
    got := Add(2, 3)
    if got != 5 {
        t.Errorf("expected 5, got %d", got)
    }
}

func TestAddNegative(t *testing.T) {
    got := Add(-2, -3)
    if got != -5 {
        t.Errorf("expected -5, got %d", got)
    }
}
// More duplication...

Use tt Loop Variable

Use tt as the conventional name for the table test variable.

// CORRECT: Use tt for test case variable
func TestValidate(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
    }{
        {name: "valid input", input: "test@example.com", wantErr: false},
        {name: "invalid input", input: "not-an-email", wantErr: true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := Validate(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}
// WRONG: Inconsistent variable naming
for _, tc := range tests {  // Use tt, not tc or testCase
    t.Run(tc.name, func(t *testing.T) {
        // ...
    })
}

Descriptive Test Names

Use clear, descriptive names for test cases that explain what is being tested.

// CORRECT: Descriptive test case names
tests := []struct {
    name string
    // ...
}{
    {name: "empty input returns error"},
    {name: "valid email passes validation"},
    {name: "email without @ symbol fails"},
    {name: "concurrent access is thread-safe"},
}
// WRONG: Unclear test case names
tests := []struct {
    name string
    // ...
}{
    {name: "test1"},
    {name: "case2"},
    {name: "good"},
    {name: "bad"},
}

Always Use t.Run for Subtests

Use t.Run to create subtests for better test organization and parallel execution.

// CORRECT: t.Run for subtests
func TestUserService(t *testing.T) {
    tests := []struct {
        name string
        id   string
        want *User
    }{
        {name: "existing user", id: "123", want: &User{ID: "123", Name: "Alice"}},
        {name: "non-existent user", id: "999", want: nil},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := service.GetUser(tt.id)
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("GetUser() = %v, want %v", got, tt.want)
            }
        })
    }
}
// WRONG: No subtests, harder to identify failures
func TestUserService(t *testing.T) {
    tests := []struct {
        id   string
        want *User
    }{
        {id: "123", want: &User{ID: "123"}},
        {id: "999", want: nil},
    }

    for _, tt := range tests {
        got := service.GetUser(tt.id)  // Can't tell which case failed
        if !reflect.DeepEqual(got, tt.want) {
            t.Errorf("failed for %s", tt.id)
        }
    }
}

Test File Naming and Organization

Use _test.go Suffix

Test files must end with _test.go and be in the same package or _test package.

// user.go
package user

type User struct {
    ID   string
    Name string
}
// CORRECT: user_test.go (same package)
package user

import "testing"

func TestNewUser(t *testing.T) {
    u := NewUser("1", "Alice")
    if u.ID != "1" {
        t.Errorf("expected ID 1, got %s", u.ID)
    }
}
// CORRECT: user_test.go (external test package)
package user_test

import (
    "testing"
    "myapp/user"
)

func TestUserAPI(t *testing.T) {
    u := user.NewUser("1", "Alice")
    // Test exported API only
}

Test Function Naming Convention

Test functions must start with Test, benchmarks with Benchmark, examples with Example.

// CORRECT: Test function naming
func TestUserValidation(t *testing.T) {}
func TestUser_SetName(t *testing.T) {}
func TestUserService_GetUser_NotFound(t *testing.T) {}

func BenchmarkUserValidation(b *testing.B) {}
func BenchmarkUser_SetName(b *testing.B) {}

func ExampleUser_SetName() {}
// WRONG: Invalid function names
func userValidation(t *testing.T) {}      // Must start with Test
func Test_user_validation(t *testing.T) {} // Use TestUserValidation
func testUserValidation(t *testing.T) {}   // Must start with capital T

testify Package Usage

require vs assert

Use require for critical assertions that should stop the test immediately. Use assert for non-critical checks.

// CORRECT: require for critical setup, assert for checks
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestUserService(t *testing.T) {
    db, err := setupTestDB()
    require.NoError(t, err, "failed to setup test DB")  // Stop if setup fails
    require.NotNil(t, db)

    svc := NewService(db)
    user, err := svc.GetUser("123")
    require.NoError(t, err)

    assert.Equal(t, "123", user.ID)          // Continue even if fails
    assert.Equal(t, "Alice", user.Name)      // Can check multiple fields
    assert.True(t, user.Active)
}
// WRONG: Using assert for critical setup
func TestUserService(t *testing.T) {
    db, err := setupTestDB()
    assert.NoError(t, err)  // Test continues even if DB setup failed!

    svc := NewService(db)   // nil pointer panic if db is nil
    user, err := svc.GetUser("123")
}

require.NoError for Error Checks

Use require.NoError for clear error checking in tests.

// CORRECT: require.NoError
func TestLoadConfig(t *testing.T) {
    cfg, err := LoadConfig("config.json")
    require.NoError(t, err, "LoadConfig should not return error")
    require.NotNil(t, cfg)

    assert.Equal(t, "localhost", cfg.Host)
    assert.Equal(t, 8080, cfg.Port)
}
// WRONG: Manual error checking in tests
func TestLoadConfig(t *testing.T) {
    cfg, err := LoadConfig("config.json")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)  // Verbose
    }
    if cfg == nil {
        t.Fatal("cfg should not be nil")
    }
}

testify Table Tests

Combine table-driven tests with testify assertions.

// CORRECT: Table tests with testify
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
        errMsg  string
    }{
        {
            name:    "valid email",
            email:   "user@example.com",
            wantErr: false,
        },
        {
            name:    "missing @ symbol",
            email:   "userexample.com",
            wantErr: true,
            errMsg:  "invalid email format",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if tt.wantErr {
                require.Error(t, err)
                assert.Contains(t, err.Error(), tt.errMsg)
            } else {
                require.NoError(t, err)
            }
        })
    }
}

HTTP Testing

Use httptest.NewServer for Integration Tests

Use httptest.NewServer to create a test HTTP server for integration testing.

// CORRECT: httptest.NewServer for integration tests
func TestAPIClient_GetUser(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Equal(t, "/users/123", r.URL.Path)
        assert.Equal(t, "GET", r.Method)

        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]string{
            "id":   "123",
            "name": "Alice",
        })
    }))
    defer server.Close()

    client := NewClient(server.URL)
    user, err := client.GetUser("123")
    require.NoError(t, err)
    assert.Equal(t, "123", user.ID)
    assert.Equal(t, "Alice", user.Name)
}

Use httptest.NewRecorder for Unit Tests

Use httptest.NewRecorder to test HTTP handlers without starting a server.

// CORRECT: httptest.NewRecorder for handler unit tests
func TestGetUserHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/users/123", nil)
    rec := httptest.NewRecorder()

    handler := GetUserHandler(mockUserService)
    handler.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)

    var response map[string]string
    err := json.NewDecoder(rec.Body).Decode(&response)
    require.NoError(t, err)
    assert.Equal(t, "123", response["id"])
}
// WRONG: Creating actual server for unit tests
func TestGetUserHandler(t *testing.T) {
    server := httptest.NewServer(GetUserHandler(mockUserService))  // Overkill
    defer server.Close()

    resp, err := http.Get(server.URL + "/users/123")
    // Unnecessary complexity for unit test
}

Mocking with gomock

Generate Mocks with mockgen

Use mockgen to generate mocks for interfaces. Store mocks in internal/mocks or package_test.go.

# Install mockgen
go install go.uber.org/mock/mockgen@latest

# Generate mocks
mockgen -source=user.go -destination=internal/mocks/user_mock.go -package=mocks
// user.go - Interface to mock
package user

import "context"

type Repository interface {
    GetUser(ctx context.Context, id string) (*User, error)
    SaveUser(ctx context.Context, user *User) error
}
// CORRECT: Using generated mocks
package user_test

import (
    "context"
    "testing"
    "myapp/internal/mocks"
    "myapp/user"
    "go.uber.org/mock/gomock"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestUserService_GetUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockRepository(ctrl)
    mockRepo.EXPECT().
        GetUser(gomock.Any(), "123").
        Return(&user.User{ID: "123", Name: "Alice"}, nil)

    svc := user.NewService(mockRepo)
    u, err := svc.GetUser(context.Background(), "123")

    require.NoError(t, err)
    assert.Equal(t, "123", u.ID)
}

Store Mocks in internal/mocks Directory

Keep generated mocks organized in internal/mocks/ directory.

myapp/
  internal/
    mocks/
      user_mock.go
      payment_mock.go
  user/
    user.go
    user_test.go
// go:generate directive in source file
//go:generate mockgen -source=user.go -destination=../internal/mocks/user_mock.go -package=mocks
package user

Use gomock.InOrder for Sequential Expectations

Use gomock.InOrder when the order of method calls matters.

// CORRECT: InOrder for sequential calls
func TestUserService_CreateAndNotify(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockRepository(ctrl)
    mockNotifier := mocks.NewMockNotifier(ctrl)

    gomock.InOrder(
        mockRepo.EXPECT().SaveUser(gomock.Any(), gomock.Any()).Return(nil),
        mockNotifier.EXPECT().SendWelcome(gomock.Any(), gomock.Any()).Return(nil),
    )

    svc := user.NewService(mockRepo, mockNotifier)
    err := svc.CreateUser(context.Background(), &user.User{Name: "Alice"})
    require.NoError(t, err)
}

Test Helpers

Mark Test Helpers with t.Helper

Use t.Helper() in test helper functions to report errors at the caller location.

// CORRECT: Test helper with t.Helper()
func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()  // Errors reported in calling test, not here

    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("failed to open test DB: %v", err)
    }

    if err := runMigrations(db); err != nil {
        t.Fatalf("failed to run migrations: %v", err)
    }

    return db
}

func TestUserRepository(t *testing.T) {
    db := setupTestDB(t)  // Error reported here if setup fails
    defer db.Close()

    // Test implementation
}
// WRONG: Helper without t.Helper()
func setupTestDB(t *testing.T) *sql.DB {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("failed: %v", err)  // Error line points here, not caller
    }
    return db
}

Cleanup with t.Cleanup

Use t.Cleanup for test cleanup instead of defer when cleanup depends on test context.

// CORRECT: Using t.Cleanup
func TestWithTempFile(t *testing.T) {
    tmpfile, err := os.CreateTemp("", "test")
    require.NoError(t, err)

    t.Cleanup(func() {
        os.Remove(tmpfile.Name())
    })

    // Test using tmpfile
    // Cleanup runs even if test fails
}

Benchmarks

Benchmark Function Naming

Benchmark functions must start with Benchmark and take *testing.B parameter.

// CORRECT: Benchmark naming
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

func BenchmarkUserValidation(b *testing.B) {
    user := &User{ID: "123", Email: "test@example.com"}
    b.ResetTimer()  // Reset after setup

    for i := 0; i < b.N; i++ {
        user.Validate()
    }
}
// WRONG: Invalid benchmark naming
func benchmarkAdd(b *testing.B) {}     // Must start with capital B
func TestBenchmarkAdd(b *testing.B) {} // Don't mix Test and Benchmark

Use b.ResetTimer After Setup

Call b.ResetTimer() after expensive setup to exclude setup time from benchmark.

// CORRECT: ResetTimer after setup
func BenchmarkDatabaseQuery(b *testing.B) {
    db := setupTestDB(b)
    defer db.Close()

    b.ResetTimer()  // Don't include setup time

    for i := 0; i < b.N; i++ {
        db.Query("SELECT * FROM users WHERE id = ?", i)
    }
}
// WRONG: Including setup in benchmark
func BenchmarkDatabaseQuery(b *testing.B) {
    for i := 0; i < b.N; i++ {
        db := setupTestDB(b)  // Setup repeated b.N times!
        db.Query("SELECT * FROM users WHERE id = ?", i)
        db.Close()
    }
}

Report Allocations with b.ReportAllocs

Use b.ReportAllocs() to track memory allocations in benchmarks.

// CORRECT: Report allocations
func BenchmarkStringConcat(b *testing.B) {
    b.ReportAllocs()  // Shows allocs/op in results

    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    b.ReportAllocs()

    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}
# Run benchmarks with memory stats
go test -bench=. -benchmem

# Compare benchmarks
go test -bench=. -benchmem -count=5 | tee old.txt
# Make changes
go test -bench=. -benchmem -count=5 | tee new.txt
benchstat old.txt new.txt

Table-Driven Benchmarks

Use table-driven pattern for benchmarks with multiple scenarios.

// CORRECT: Table-driven benchmarks
func BenchmarkValidation(b *testing.B) {
    benchmarks := []struct {
        name  string
        input string
    }{
        {name: "short email", input: "a@b.c"},
        {name: "normal email", input: "user@example.com"},
        {name: "long email", input: "very.long.email.address@subdomain.example.com"},
    }

    for _, bm := range benchmarks {
        b.Run(bm.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                ValidateEmail(bm.input)
            }
        })
    }
}

Fuzzing

Use f.Fuzz for Fuzz Testing

Use Go's built-in fuzzing to discover edge cases.

// CORRECT: Fuzz test
func FuzzParseEmail(f *testing.F) {
    // Seed corpus
    f.Add("user@example.com")
    f.Add("test@test.org")
    f.Add("invalid")

    f.Fuzz(func(t *testing.T, email string) {
        result, err := ParseEmail(email)

        // Invariants that should always hold
        if err == nil {
            require.NotEmpty(t, result.User)
            require.NotEmpty(t, result.Domain)
            require.Contains(t, email, "@")
        }
    })
}
# Run fuzz tests
go test -fuzz=FuzzParseEmail -fuzztime=30s

# Run with seed corpus only
go test -fuzz=FuzzParseEmail -fuzztime=0s

Seed Corpus for Fuzzing

Provide good seed inputs to guide fuzzing toward interesting cases.

// CORRECT: Good seed corpus
func FuzzJSONParse(f *testing.F) {
    f.Add(`{"name":"Alice","age":30}`)
    f.Add(`{"name":"Bob"}`)
    f.Add(`{}`)
    f.Add(`{"nested":{"value":true}}`)
    f.Add(`[]`)  // Invalid but interesting

    f.Fuzz(func(t *testing.T, data string) {
        var v interface{}
        _ = json.Unmarshal([]byte(data), &v)
        // Should not panic
    })
}

Parallel Tests

Use t.Parallel for Independent Tests

Mark independent tests as parallel to speed up test execution.

// CORRECT: Parallel tests
func TestUserValidation(t *testing.T) {
    t.Parallel()  // Can run in parallel with other tests

    tests := []struct {
        name string
        user *User
        want error
    }{
        {name: "valid user", user: &User{ID: "1", Email: "a@b.c"}, want: nil},
        {name: "missing email", user: &User{ID: "1"}, want: ErrInvalidEmail},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()  // Subtests also parallel
            got := tt.user.Validate()
            assert.Equal(t, tt.want, got)
        })
    }
}
// WRONG: t.Parallel() on tests that share state
func TestSharedState(t *testing.T) {
    t.Parallel()  // Don't use if test modifies global state

    globalCounter = 0  // Race condition!
    globalCounter++
}

Golden Files

Use testdata Directory for Test Fixtures

Store test fixtures and golden files in testdata/ directory.

myapp/
  parser/
    parser.go
    parser_test.go
    testdata/
      valid_input.json
      invalid_input.json
      expected_output.golden
// CORRECT: Using golden files
func TestParser(t *testing.T) {
    input, err := os.ReadFile("testdata/valid_input.json")
    require.NoError(t, err)

    got, err := Parse(input)
    require.NoError(t, err)

    golden, err := os.ReadFile("testdata/expected_output.golden")
    require.NoError(t, err)

    assert.Equal(t, string(golden), got.String())
}

Update Golden Files with Flag

Provide a flag to update golden files when output changes intentionally.

// CORRECT: Golden file update flag
var update = flag.Bool("update", false, "update golden files")

func TestRender(t *testing.T) {
    got := Render(data)
    goldenPath := "testdata/output.golden"

    if *update {
        err := os.WriteFile(goldenPath, []byte(got), 0644)
        require.NoError(t, err)
    }

    golden, err := os.ReadFile(goldenPath)
    require.NoError(t, err)
    assert.Equal(t, string(golden), got)
}
# Update golden files
go test -update

# Normal test run
go test

Coverage

Run Tests with Coverage

Always measure test coverage and aim for meaningful coverage.

# Run tests with coverage
go test -v -race -coverprofile=coverage.out ./...

# View coverage in terminal
go tool cover -func=coverage.out

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

# Coverage by package
go test -coverprofile=coverage.out ./... && go tool cover -func=coverage.out | grep total
# Minimum coverage check in CI
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
    awk '{if ($1 < 80) exit 1}'

Focus on Meaningful Coverage

Aim for high coverage of business logic, not just line coverage.

// CORRECT: Test important paths and edge cases
func TestProcessPayment(t *testing.T) {
    tests := []struct {
        name    string
        amount  float64
        balance float64
        wantErr error
    }{
        {name: "sufficient balance", amount: 50, balance: 100, wantErr: nil},
        {name: "insufficient balance", amount: 150, balance: 100, wantErr: ErrInsufficientFunds},
        {name: "zero amount", amount: 0, balance: 100, wantErr: ErrInvalidAmount},
        {name: "negative amount", amount: -50, balance: 100, wantErr: ErrInvalidAmount},
    }
    // Test all important scenarios
}

This skill ensures comprehensive testing coverage following Go community best practices. Apply these patterns consistently to maintain high-quality, reliable code.

Stats
Stars0
Forks0
Last CommitFeb 9, 2026

Similar Skills