agentsclimarketplace

Go testing patterns

Skill CowDogMoo/squad-agents/skills/go-testing-patterns

Idiomatic Go testing patterns — table-driven tests, subtests, test helpers, mocking strategies, benchmarks, coverage, Cobra CLI testing, and what not to test. Use when writing Go tests or raising coverage on a package. Also loaded by the go-tests agent as its knowledge base.From its SKILL.md

Install
npx -y skills add CowDogMoo/squad-agents --skill go-testing-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

15.0 KB, as published. Nobody here has run it

Go Testing Patterns

A comprehensive guide to writing effective Go tests following community best practices and the Go testing philosophy. This document serves as the knowledge base for the go-tests pattern.

Table of Contents

  1. Testing Philosophy
  2. Table-Driven Tests
  3. Subtests
  4. Test Helpers
  5. Mocking Strategies
  6. Benchmarks
  7. Test Coverage
  8. Common Patterns
  9. What Not to Test
  10. Cobra CLI Testing

Testing Philosophy

Core Principles

PrincipleDescription
Simple and readableTests should be easy to understand at a glance
Test behaviorTest what the code does, not how it does it
Avoid over-mockingOnly mock when truly necessary
Clarity over DRYPrefer clarity to avoiding repetition in tests
One thing per testEach test case should verify one behavior

Simplicity Guidelines

DO:

  • Write tests that are easy to understand at a glance
  • Use inline test data rather than complex fixtures
  • Keep test setup minimal
  • Use simple assertions
  • Test one thing per test case

DON'T:

  • Create test helpers unless used in many places
  • Build complex test hierarchies
  • Over-parameterize tests
  • Add unnecessary interfaces or abstractions
  • Write tests for unlikely edge cases
  • Use reflection or advanced features unnecessarily

Table-Driven Tests

Table-driven tests are idiomatic Go when you have multiple similar test cases.

Basic Structure

func TestParseURL(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    *URL
        wantErr bool
    }{
        {
            name:  "valid http url",
            input: "http://example.com",
            want:  &URL{Scheme: "http", Host: "example.com"},
        },
        {
            name:  "valid https url",
            input: "https://example.com/path",
            want:  &URL{Scheme: "https", Host: "example.com", Path: "/path"},
        },
        {
            name:    "invalid url",
            input:   "://invalid",
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseURL(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("ParseURL() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("ParseURL() = %v, want %v", got, tt.want)
            }
        })
    }
}

When to Use Table-Driven Tests

Good for:

  • Functions with multiple input/output combinations
  • Validation functions
  • Parsing functions
  • Mathematical operations

Not needed for:

  • Single test case scenarios
  • Complex setup/teardown requirements
  • Tests with significantly different logic per case

Subtests

Basic Subtests

func TestUser(t *testing.T) {
    t.Run("creation", func(t *testing.T) {
        user := NewUser("test")
        if user.Name != "test" {
            t.Errorf("Name = %q, want %q", user.Name, "test")
        }
    })

    t.Run("validation", func(t *testing.T) {
        user := &User{}
        if err := user.Validate(); err == nil {
            t.Error("expected error for empty user")
        }
    })
}

Parallel Subtests

func TestParallel(t *testing.T) {
    tests := []struct {
        name  string
        input int
        want  int
    }{
        {"case1", 1, 2},
        {"case2", 2, 4},
        {"case3", 3, 6},
    }

    for _, tt := range tests {
        tt := tt // capture range variable
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            got := Double(tt.input)
            if got != tt.want {
                t.Errorf("Double(%d) = %d, want %d", tt.input, got, tt.want)
            }
        })
    }
}

Test Helpers

When to Create Helpers

Only create helpers when:

  • Used in multiple test files
  • Significantly reduces test code complexity
  • Makes tests more readable

Helper Pattern

// testHelper marks the function as a test helper and reports
// the caller's location on failure.
func testHelper(t *testing.T) {
    t.Helper()
    // helper logic
}

// newTestUser creates a User for testing with sensible defaults.
func newTestUser(t *testing.T, name string) *User {
    t.Helper()
    user := &User{
        ID:        1,
        Name:      name,
        CreatedAt: time.Now(),
    }
    return user
}

Cleanup Pattern

func TestWithCleanup(t *testing.T) {
    tempDir := t.TempDir() // automatically cleaned up

    // Or manual cleanup
    file, err := os.CreateTemp("", "test")
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        os.Remove(file.Name())
    })
}

Mocking Strategies

Interface-Based Mocking

// Define interface in consumer package
type UserRepository interface {
    GetByID(ctx context.Context, id string) (*User, error)
}

// Mock implementation for tests
type mockUserRepo struct {
    users map[string]*User
    err   error
}

func (m *mockUserRepo) GetByID(ctx context.Context, id string) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    return m.users[id], nil
}

func TestUserService(t *testing.T) {
    repo := &mockUserRepo{
        users: map[string]*User{"1": {ID: "1", Name: "Test"}},
    }
    service := NewUserService(repo)

    user, err := service.GetUser(context.Background(), "1")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Test" {
        t.Errorf("Name = %q, want %q", user.Name, "Test")
    }
}

When to Mock

Do mock:

  • External services (databases, APIs)
  • Time-dependent behavior
  • Random/non-deterministic behavior

Don't mock:

  • Simple value objects
  • Standard library functions
  • Internal implementation details

Benchmarks

Basic Benchmark

func BenchmarkParseURL(b *testing.B) {
    input := "https://example.com/path?query=value"
    for i := 0; i < b.N; i++ {
        ParseURL(input)
    }
}

Benchmark with Setup

func BenchmarkProcess(b *testing.B) {
    // Setup (not timed)
    data := generateTestData(1000)

    b.ResetTimer() // Reset timer after setup
    for i := 0; i < b.N; i++ {
        Process(data)
    }
}

Sub-Benchmarks

func BenchmarkCache(b *testing.B) {
    sizes := []int{100, 1000, 10000}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
            cache := NewCache(size)
            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                cache.Get("key")
            }
        })
    }
}

Memory Benchmarks

func BenchmarkAlloc(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _ = make([]byte, 1024)
    }
}

Test Coverage

Running Coverage

# Generate coverage report
go test -coverprofile=coverage.out ./...

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

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

Coverage Goals

TypeTarget
Unit tests70-80%
Critical paths90%+
Edge casesAs needed

What Coverage Doesn't Tell You

  • Quality of assertions
  • Whether edge cases are tested
  • Whether tests are meaningful
  • Whether error paths are properly tested

Common Patterns

Error Testing

func TestValidate_Errors(t *testing.T) {
    tests := []struct {
        name    string
        input   *User
        wantErr string
    }{
        {
            name:    "nil user",
            input:   nil,
            wantErr: "user is nil",
        },
        {
            name:    "empty name",
            input:   &User{},
            wantErr: "name is required",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := Validate(tt.input)
            if err == nil {
                t.Fatal("expected error, got nil")
            }
            if !strings.Contains(err.Error(), tt.wantErr) {
                t.Errorf("error = %q, want to contain %q", err.Error(), tt.wantErr)
            }
        })
    }
}

Context Testing

func TestWithContext(t *testing.T) {
    t.Run("respects cancellation", func(t *testing.T) {
        ctx, cancel := context.WithCancel(context.Background())
        cancel() // Cancel immediately

        err := DoWork(ctx)
        if !errors.Is(err, context.Canceled) {
            t.Errorf("expected context.Canceled, got %v", err)
        }
    })

    t.Run("respects timeout", func(t *testing.T) {
        ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
        defer cancel()

        err := SlowWork(ctx)
        if !errors.Is(err, context.DeadlineExceeded) {
            t.Errorf("expected context.DeadlineExceeded, got %v", err)
        }
    })
}

HTTP Handler Testing

func TestHandler(t *testing.T) {
    handler := NewHandler()

    t.Run("GET returns user", func(t *testing.T) {
        req := httptest.NewRequest("GET", "/users/1", nil)
        w := httptest.NewRecorder()

        handler.ServeHTTP(w, req)

        if w.Code != http.StatusOK {
            t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
        }

        var user User
        if err := json.Unmarshal(w.Body.Bytes(), &user); err != nil {
            t.Fatalf("failed to unmarshal: %v", err)
        }
        if user.ID != "1" {
            t.Errorf("ID = %q, want %q", user.ID, "1")
        }
    })
}

What Not to Test

Skip Testing

  • Trivial getters/setters without logic
  • Third-party code (trust the library)
  • Language features (Go's behavior)
  • Unlikely scenarios that can't reasonably occur

Example: Don't Test This

// Trivial getter - no need to test
func (u *User) Name() string {
    return u.name
}

// Simple delegation - test the underlying function instead
func (s *Service) GetUser(id string) (*User, error) {
    return s.repo.GetByID(id)
}

Example: Do Test This

// Has logic - should test
func (u *User) FullName() string {
    if u.MiddleName != "" {
        return fmt.Sprintf("%s %s %s", u.FirstName, u.MiddleName, u.LastName)
    }
    return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
}

// Has validation - should test
func (s *Service) CreateUser(u *User) error {
    if err := u.Validate(); err != nil {
        return fmt.Errorf("invalid user: %w", err)
    }
    return s.repo.Create(u)
}

Quick Reference

Test File Naming

  • foo.go → foo_test.go (strict 1:1 mapping)
  • foo_internal_test.go (package foo) for white-box tests needing unexported access — the only meaningful suffix variant in Go
  • Test functions: TestXxx(t *testing.T)
  • Benchmarks: BenchmarkXxx(b *testing.B)
  • Examples: ExampleXxx()

To separate test types (unit, integration, coverage-boost), use:

  • Build tags (//go:build integration) to gate tests by category.
  • Subtests (t.Run("edge-case/empty-input", ...)) to group related cases within a single _test.go file.
  • _internal_test.go (package foo) vs _test.go (package foo_test) to distinguish white-box from black-box tests.

Common Assertions

// Equality
if got != want {
    t.Errorf("got %v, want %v", got, want)
}

// Deep equality
if !reflect.DeepEqual(got, want) {
    t.Errorf("got %+v, want %+v", got, want)
}

// Error presence
if err != nil {
    t.Fatalf("unexpected error: %v", err)
}

// Error expected
if err == nil {
    t.Fatal("expected error, got nil")
}

Cobra CLI Testing

Testing Cobra commands requires extra care because rootCmd is typically a package-level singleton whose state persists across subtests.

Per-Subtest Isolation

func TestExecute(t *testing.T) {
    // Subtests share rootCmd — do not add t.Parallel().
    tests := []struct {
        name    string
        args    []string
        wantErr string // empty means no error expected
        wantOut string // substring expected in stdout
    }{
        {"help flag", []string{"--help"}, "", "Usage:"},
        {"unknown flag", []string{"--bogus"}, "unknown flag", ""},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Fresh buffer per subtest.
            var buf bytes.Buffer
            rootCmd.SetOut(&buf)
            rootCmd.SetErr(&buf)
            rootCmd.SetArgs(tt.args)
            t.Cleanup(func() {
                rootCmd.SetOut(os.Stdout)
                rootCmd.SetErr(os.Stderr)
                rootCmd.SetArgs(nil)
            })

            err := Execute()

            if tt.wantErr != "" {
                if err == nil {
                    t.Fatalf("expected error containing %q, got nil", tt.wantErr)
                }
                if !strings.Contains(err.Error(), tt.wantErr) &&
                    !strings.Contains(buf.String(), tt.wantErr) {
                    t.Fatalf("error %q does not contain %q", err, tt.wantErr)
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if tt.wantOut != "" && !strings.Contains(buf.String(), tt.wantOut) {
                t.Fatalf("output missing %q:\n%s", tt.wantOut, buf.String())
            }
        })
    }
}

Key points:

  • t.Cleanup inside every subtest — restores SetOut, SetErr, and SetArgs so the next subtest starts clean.
  • Fresh bytes.Buffer per subtest — never share a buffer.
  • Assert on error content — check err.Error() or stderr for the expected substring, not just err != nil.
  • Comment explaining no t.Parallel() — the singleton command is shared mutable state.

Variable Shadowing

Never declare a local variable that shadows a package-level name:

// BAD — shadows the package-level `output` flag variable
output := string(data)

// GOOD — distinct name
got := string(data)

References


Last updated: 2026-01-10

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.