Claude Code Plugins

Community-maintained marketplace

Feedback

Principles and patterns for writing effective Go tests. Use during implementation for test structure guidance, choosing between table-driven tests vs testify suites, or deciding between real implementations vs mocks. Emphasizes testing public API only with 100% coverage on leaf types.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name testing
description Principles and patterns for writing effective Go tests. Use during implementation for test structure guidance, choosing between table-driven tests vs testify suites, or deciding between real implementations vs mocks. Emphasizes testing public API only with 100% coverage on leaf types.

Testing Principles

Principles and patterns for writing effective Go tests.

When to Use

  • During implementation (tests + code in parallel)
  • When testing strategy is unclear
  • When structuring table-driven tests or testify suites
  • When choosing between real implementations vs mocks

Testing Philosophy

Test only the public API

  • Use pkg_test package name
  • Test types through their constructors
  • No testing private methods/functions

Prefer real implementations over mocks

  • Use HTTP test servers
  • Use temp files/directories
  • Use in-memory databases
  • Test with actual dependencies (integration-style)

Coverage targets

  • Leaf types: 100% unit test coverage
  • Orchestrating types: Integration tests

Workflow

1. Identify What to Test

  • Leaf types: Self-contained types with logic

    • Test all public methods
    • Test validation in constructors
    • Cover happy path + edge cases + errors
  • Orchestrating types: Types that coordinate others

    • Test workflows/seams between components
    • Use real implementations, not mocks

2. Choose Test Structure

Table-Driven Tests - Use when:

  • Each test case has cyclomatic complexity = 1
  • Testing simple, focused scenarios
  • No conditionals needed in test cases

Testify Suites - Use ONLY when:

  • Complex infrastructure setup needed (mock servers, DBs)
  • Expensive setup/teardown shared across tests
  • OpenTelemetry or similar complex testing infrastructure

3. Write Tests in pkg_test Package

package user_test  // External package

import (
    "testing"
    "github.com/yourorg/project/user"
    "github.com/stretchr/testify/assert"
)

func TestUserService_CreateUser(t *testing.T) {
    // Test public API only
}

4. Use Real Implementations

// ✅ Real implementations
repo := user.NewInMemoryRepository()
notifier := user.NewTestEmailer()  // Writes to buffer, not real email

svc, err := user.NewUserService(repo, notifier)
require.NoError(t, err)

// Test with real dependencies
err = svc.CreateUser(ctx, testUser)
assert.NoError(t, err)

5. Avoid Common Pitfalls

  • ❌ No time.Sleep (use channels/waitgroups)
  • ❌ No conditionals in test cases (complexity = 1)
  • ❌ No testing private methods
  • ❌ No heavy mocking

Test Patterns

Pattern 1: Table-Driven Tests

func TestNewUserID(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    user.UserID
        wantErr bool
    }{
        {
            name:    "valid ID",
            input:   "usr_123",
            want:    user.UserID("usr_123"),
            wantErr: false,
        },
        {
            name:    "empty ID",
            input:   "",
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := user.NewUserID(tt.input)

            if tt.wantErr {
                assert.Error(t, err)
                return
            }

            assert.NoError(t, err)
            assert.Equal(t, tt.want, got)
        })
    }
}

Key: ALWAYS use named struct fields (linter reorders fields)

Pattern 2: Testify Suite (Complex Setup)

type ServiceSuite struct {
    suite.Suite
    server   *httptest.Server
    db       *sql.DB
    tempDir  string
}

func (s *ServiceSuite) SetupSuite() {
    // Expensive setup once for all tests
    s.server = httptest.NewServer(handler)
    s.db = setupTestDB()
}

func (s *ServiceSuite) TearDownSuite() {
    s.server.Close()
    s.db.Close()
}

func (s *ServiceSuite) SetupTest() {
    // Per-test setup
    s.tempDir, _ = os.MkdirTemp("", "test")
}

func (s *ServiceSuite) TearDownTest() {
    os.RemoveAll(s.tempDir)
}

func (s *ServiceSuite) TestSomething() {
    // Use s.server, s.db, s.tempDir
}

func TestServiceSuite(t *testing.T) {
    suite.Run(t, new(ServiceSuite))
}

Pattern 3: Synchronization (No time.Sleep)

// ✅ Use channels
func TestAsyncWork(t *testing.T) {
    done := make(chan struct{})

    go func() {
        doWork()
        close(done)
    }()

    select {
    case <-done:
        // Success
    case <-time.After(1 * time.Second):
        t.Fatal("timeout")
    }
}

// ✅ Use waitgroups
func TestConcurrentWork(t *testing.T) {
    var wg sync.WaitGroup
    wg.Add(10)

    for i := 0; i < 10; i++ {
        go func() {
            defer wg.Done()
            doWork()
        }()
    }

    wg.Wait()
    // Assert results
}

Output Format

After writing tests:

✅ TESTING COMPLETE

Test Coverage:
- user/user_id_test.go: 100% (NewUserID, String)
- user/email_test.go: 100% (NewEmail, Validate)
- user/service_test.go: 92% (CreateUser, GetUser, UpdateUser)

Test Structure:
- Table-driven tests: 15 test cases
- Integration tests: 3 workflows
- Real implementations used: InMemoryRepository, TestEmailer

Test Execution:
$ go test ./user/...
ok      github.com/yourorg/project/user  0.123s

Next Steps:
1. Run linter: task lintwithfix
2. If linter fails → use @refactoring skill
3. If linter passes → use @pre-commit-review skill

Key Principles

See reference.md for:

  • Table-driven test patterns
  • Testify suite guidelines
  • Real implementations over mocks
  • Synchronization techniques
  • Coverage strategies

Testing Checklist

Before considering tests complete:

  • All tests in pkg_test package
  • Testing public API only (no private methods)
  • Table-driven tests use named struct fields
  • No conditionals in test cases (complexity = 1)
  • Using real implementations, not mocks
  • No time.Sleep (using channels/waitgroups)
  • Leaf types have 100% coverage
  • Integration tests cover orchestrating types

See reference.md for complete testing guidelines and examples.