Files
gh-buzzdan-ai-coding-rules-…/skills/testing/reference.md
2025-11-29 18:02:42 +08:00

17 KiB

Testing Reference

Complete guide to Go testing principles and patterns.

Core Testing Principles

1. Test Only Public API

  • Use pkg_test package name - Forces external perspective
  • Test types via constructors - No direct struct initialization
  • No testing private methods - If you need to test it, make it public or rethink design
// ✅ Good
package user_test

import "github.com/yourorg/project/user"

func TestService_CreateUser(t *testing.T) {
    svc, _ := user.NewUserService(repo, notifier)
    err := svc.CreateUser(ctx, testUser)
    // ...
}

2. Avoid Mocks - Use Real Implementations

Instead of mocks, use:

  • HTTP test servers (httptest package)
  • Temp files/directories (os.CreateTemp, os.MkdirTemp)
  • In-memory databases (SQLite in-memory, or custom implementations)
  • Test implementations (TestEmailer that writes to buffer)

Benefits:

  • Tests are more reliable
  • Tests verify actual behavior
  • Easier to maintain

3. Coverage Strategy

Leaf Types (self-contained):

  • Target: 100% unit test coverage
  • Why: Core logic must be bulletproof

Orchestrating Types (coordinate others):

  • Target: Integration test coverage
  • Why: Test seams between components

Goal: Most logic in leaf types (easier to test and maintain)


Table-Driven Tests

When to Use

  • Each test case has cyclomatic complexity = 1
  • No conditionals inside t.Run()
  • Simple, focused testing scenarios

Anti-Pattern: wantErr bool

DO NOT use wantErr bool pattern - it violates complexity = 1 rule:

// ❌ BAD - Has conditionals (complexity > 1)
func TestNewUserID(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    UserID
        wantErr bool  // ❌ Anti-pattern
    }{
        {name: "valid ID", input: "usr_123", want: 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 := NewUserID(tt.input)
            if tt.wantErr {  // ❌ Conditional
                assert.Error(t, err)
                return
            }
            assert.NoError(t, err)
            assert.Equal(t, tt.want, got)
        })
    }
}

Correct Pattern: Separate Functions

Always separate success and error cases:

// ✅ Success cases - Complexity = 1
func TestNewUserID_Success(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  UserID
    }{
        {name: "valid ID", input: "usr_123", want: UserID("usr_123")},
        {name: "with numbers", input: "usr_456", want: UserID("usr_456")},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := NewUserID(tt.input)
            require.NoError(t, err)  // ✅ No conditionals
            assert.Equal(t, tt.want, got)
        })
    }
}

// ✅ Error cases - Complexity = 1
func TestNewUserID_Error(t *testing.T) {
    tests := []struct {
        name  string
        input string
    }{
        {name: "empty ID", input: ""},
        {name: "whitespace only", input: "   "},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := NewUserID(tt.input)
            assert.Error(t, err)  // ✅ No conditionals
        })
    }
}

Critical Rule: Named Struct Fields

ALWAYS use named struct fields - Linter reorders fields, breaking unnamed initialization:

// ❌ BAD - Breaks when linter reorders fields
tests := []struct {
    name   string
    input  int
    want   string
}{
    {"test1", 42, "result"},  // Will break
}

// ✅ GOOD - Works regardless of field order
tests := []struct {
    name   string
    input  int
    want   string
}{
    {name: "test1", input: 42, want: "result"},  // Always works
}

Testify Suites

When to Use

ONLY for complex test infrastructure setup:

  • Mock HTTP servers
  • Database connections
  • OpenTelemetry testing setup
  • Temporary files/directories needing cleanup
  • Shared expensive setup/teardown

When NOT to Use

  • Simple unit tests (use table-driven instead)
  • Tests without complex setup

Pattern

package user_test

import (
    "net/http/httptest"
    "testing"
    "github.com/stretchr/testify/suite"
)

type ServiceSuite struct {
    suite.Suite
    server   *httptest.Server
    svc      *user.UserService
}

func (s *ServiceSuite) SetupSuite() {
    s.server = httptest.NewServer(testHandler)
}

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

func (s *ServiceSuite) SetupTest() {
    s.svc = user.NewUserService(s.server.URL)
}

func (s *ServiceSuite) TestCreateUser() {
    err := s.svc.CreateUser(ctx, testUser)
    s.NoError(err)
}

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

Synchronization in Tests

Never Use time.Sleep

Use channels or WaitGroups instead.

Use Channels

func TestAsyncOperation(t *testing.T) {
    done := make(chan struct{})

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

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

Use WaitGroups

func TestConcurrentOperations(t *testing.T) {
    var wg sync.WaitGroup
    results := make([]string, 10)

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            results[index] = doWork(index)
        }(i)
    }

    wg.Wait()
    // Assert on results
}

Test Organization

File Structure

user/
├── user.go
├── user_test.go          # Tests for user.go (pkg_test)
├── service.go
├── service_test.go       # Tests for service.go (pkg_test)

Package Naming

// ✅ External package - tests public API only
package user_test

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

Real Implementation Patterns

In-Memory Repository

package user

type InMemoryRepository struct {
    mu    sync.RWMutex
    users map[UserID]User
}

func NewInMemoryRepository() *InMemoryRepository {
    return &InMemoryRepository{
        users: make(map[UserID]User),
    }
}

func (r *InMemoryRepository) Save(ctx context.Context, u User) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.users[u.ID] = u
    return nil
}

func (r *InMemoryRepository) Get(ctx context.Context, id UserID) (*User, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    u, ok := r.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return &u, nil
}

Test Email Sender

package user

import (
    "bytes"
    "fmt"
    "sync"
)

type TestEmailer struct {
    mu     sync.Mutex
    buffer bytes.Buffer
}

func NewTestEmailer() *TestEmailer {
    return &TestEmailer{}
}

func (e *TestEmailer) Send(to Email, subject, body string) error {
    e.mu.Lock()
    defer e.mu.Unlock()
    fmt.Fprintf(&e.buffer, "To: %s\nSubject: %s\n%s\n\n", to, subject, body)
    return nil
}

func (e *TestEmailer) SentEmails() string {
    e.mu.Lock()
    defer e.mu.Unlock()
    return e.buffer.String()
}

Testable Examples (GoDoc Examples)

When to Add

  • Non-trivial types
  • Types with validation
  • Common usage patterns

Pattern

// Example_UserID demonstrates basic usage.
func Example_UserID() {
    id, _ := user.NewUserID("usr_123")
    fmt.Println(id)
    // Output: usr_123
}

// Example_UserID_validation shows validation behavior.
func Example_UserID_validation() {
    _, err := user.NewUserID("")
    fmt.Println(err != nil)
    // Output: true
}

Testing Checklist

Before Considering Tests Complete

Structure:

  • Tests in pkg_test package
  • Testing public API only
  • Table-driven tests use named fields
  • No conditionals in test cases

Implementation:

  • Using real implementations, not mocks
  • No time.Sleep (using channels/waitgroups)
  • Testify suites only for complex setup

Coverage:

  • Leaf types: 100% unit test coverage
  • Orchestrating types: Integration tests
  • Happy path, edge cases, error cases covered

Summary

The Golden Rule: Cyclomatic complexity = 1 in all test cases

Test Structure Choices:

  • Table-driven tests: Simple, focused scenarios
  • Testify suites: Complex infrastructure setup only

Test Philosophy:

  • Test only public API (pkg_test package)
  • Use real implementations, not mocks
  • Leaf types: 100% coverage
  • Orchestrating types: Integration tests

Common Pitfalls to Avoid:

  • Testing private methods
  • Heavy mocking
  • time.Sleep in tests
  • Conditionals in test cases
  • Unnamed struct fields in table tests

Example Files - Reusable Testing Patterns

The following example files contain transferable patterns that apply to many scenarios, not just the specific technologies shown. Claude should read these files based on the pattern needed, not the specific technology mentioned.

Pattern 1: In-Memory Test Harness (Level 1)

File: examples/nats-in-memory.md

Pattern: Using official test harnesses from Go libraries

When to read:

  • Need to test with ANY service that provides an official Go test harness
  • Testing message queues, databases, caches, or any service with in-memory test mode
  • Want to avoid Docker but need realistic service behavior

Applies to:

  • NATS (shown in example) - Message queue with official test harness
  • Redis - github.com/alicebob/miniredis pure Go in-memory Redis
  • MongoDB - github.com/tryvium-travels/memongo in-memory MongoDB
  • PostgreSQL - github.com/jackc/pgx/v5 with pgx mock
  • Any Go library with test package - Check if dependency has /test package

Key techniques to adapt:

  • Wrapping official harness with clean API
  • Free port allocation for parallel tests
  • Clean lifecycle management (Setup/Teardown)
  • Thread-safe initialization

Pattern 2: Binary Dependency Management (Level 2)

File: examples/victoria-metrics.md

Pattern: Download, manage, and run ANY standalone binary for testing

When to read:

  • Need to test against ANY external binary executable
  • No in-memory option available
  • Want production-like testing without Docker

Applies to:

  • Victoria Metrics (shown in example) - Metrics database
  • Prometheus - Metrics and alerting
  • Grafana - Dashboards and visualization
  • Any database binaries - PostgreSQL, MySQL, Redis, etc.
  • Any CLI tools - Language servers, formatters, linters
  • Custom binaries - Your own services or third-party tools

Key techniques to adapt:

  • OS/ARCH detection (runtime.GOOS, runtime.GOARCH)
  • Thread-safe binary downloads with double-check locking
  • Health check polling with retries
  • Graceful shutdown with sync.Once
  • Free port allocation
  • Temp directory management
  • Version management via environment variables

Pattern 3: Mock Server with Generic DSL (Level 1)

File: examples/jsonrpc-mock.md

Pattern: Building generic mock servers with configurable responses using AddMockResponse()

When to read:

  • Need to mock ANY request/response protocol
  • Want readable test setup with DSL
  • Testing clients that call external APIs

Applies to:

  • JSON-RPC (shown in example) - RPC over HTTP
  • REST APIs - Use same pattern with route matching
  • GraphQL - Configure response per query
  • gRPC - Adapt for protobuf messages
  • WebSocket - Mock message responses
  • Any HTTP-based protocol - SOAP, XML-RPC, custom protocols

Key techniques to adapt:

  • Generic AddMockResponse(identifier, response) pattern
  • Using httptest.Server as foundation
  • Query/request tracking for assertions
  • Configuration-based response mapping
  • Thread-safe response storage

Pattern 4: Bidirectional Streaming with Rich DSL (Level 1)

File: examples/grpc-bufconn.md

Pattern: In-memory bidirectional communication with rich client/server mocks

When to read:

  • Testing ANY bidirectional streaming protocol
  • Need full-duplex communication in tests
  • Want to avoid network I/O

Applies to:

  • gRPC (shown in example) - Uses bufconn for in-memory
  • WebSockets - Adapt bufconn pattern
  • TCP streams - Custom protocols over TCP
  • Unix sockets - Inter-process communication
  • Any streaming protocol - Server-Sent Events, HTTP/2 streams

Key techniques to adapt:

  • bufconn for in-memory connections (gRPC-specific, but concept applies)
  • Rich mock objects with helper methods
  • Thread-safe state tracking with mutexes
  • Assertion helpers (ListenToStreamAndAssert())
  • When testing server → mock the clients
  • When testing client → mock the server

Pattern 5: HTTP DSL and Builder Pattern (Level 1)

File: examples/httptest-dsl.md

Pattern: Building readable test infrastructure with DSL wrappers over stdlib

When to read:

  • Want to wrap ANY test infrastructure with clean DSL
  • Need fluent, readable test setup
  • Building reusable test utilities

Applies to:

  • HTTP mocking (shown in example) - httptest.Server wrapper
  • Any test infrastructure - Databases, queues, file systems
  • Test data builders - Fluent APIs for creating test data
  • Custom test harnesses - Wrapping complex setups

Key techniques to adapt:

  • Builder pattern with method chaining
  • Fluent API design (OnGET().RespondJSON())
  • Separating configuration from execution
  • Type-safe builders with Go generics
  • Hiding complexity behind clean interfaces

Pattern 6: Test Organization and Structure

File: examples/test-organization.md

When to read:

  • Setting up test structure for new projects
  • Adding build tags for integration tests
  • Configuring CI/CD for tests
  • Creating testutils package structure

Universal patterns (not technology-specific):

  • File organization (pkg_test package naming)
  • Build tags (//go:build integration)
  • Makefile/Taskfile structure
  • CI/CD configuration
  • testutils package layout

Pattern 7: Integration Test Workflows

File: examples/integration-patterns.md

When to read:

  • Testing component interactions across package boundaries
  • Need patterns for Service + Repository testing
  • Testing workflows that span multiple components

Universal patterns:

  • Pattern 1: Service + Repository with in-memory deps
  • Pattern 2: Testing with real external services
  • Pattern 3: Multi-component workflow with testify suites
  • Dependency priority (in-memory > binary > test-containers)

Pattern 8: System Test (Black Box)

File: examples/system-patterns.md

When to read:

  • Writing black-box end-to-end tests
  • Testing via CLI or API
  • Need tests that work without Docker

Universal patterns:

  • CLI testing with exec.Command
  • API testing with HTTP client
  • Dependency injection architecture
  • Pure Go testing (no Docker)

How Claude Should Use These Files

Pattern-Based Reading Rules

When user needs to test with external dependencies:

  1. Has official Go test harness? → Read nats-in-memory.md

    • "Test with Redis/MongoDB/PostgreSQL/NATS"
    • "Avoid Docker but need real service"
    • Look for inspiration on wrapping official harnesses
  2. Need to download/run binary? → Read victoria-metrics.md

    • "Test with Prometheus/Grafana/any binary"
    • "Manage binary dependencies"
    • Learn OS/ARCH detection, download patterns, health checks
  3. Need to mock request/response? → Read jsonrpc-mock.md

    • "Mock REST/GraphQL/RPC/any HTTP API"
    • "Build mock with DSL"
    • Learn generic AddMockResponse() pattern
  4. Need bidirectional streaming? → Read grpc-bufconn.md

    • "Test gRPC/WebSocket/streaming protocol"
    • "In-memory bidirectional communication"
    • Learn rich mock patterns, thread-safe state
  5. Want readable test DSL? → Read httptest-dsl.md

    • "Build fluent test API"
    • "Wrap test infrastructure"
    • Learn builder pattern, method chaining

When user asks about test structure:

  • "How should I organize tests?" → Read test-organization.md
  • "How do I write integration tests?" → Read integration-patterns.md
  • "How do I write system tests?" → Read system-patterns.md

Key Principle

Examples show specific technologies (NATS, Victoria Metrics, JSON-RPC) but teach transferable patterns.

Claude should:

  1. Identify the pattern needed (harness, binary, mock DSL, etc.)
  2. Read the example file that demonstrates that pattern
  3. Adapt the techniques to the user's specific technology
  4. Use the example as a template, not a literal solution

Default Behavior (No Example Needed)

For simple scenarios, use the core patterns in this file:

  • Basic table-driven tests → Use patterns from this file
  • Simple testify suites → Use patterns from this file
  • Basic synchronization → Use patterns from this file
  • Simple in-memory implementations → Use InMemoryRepository/TestEmailer from this file

Read example files when patterns/techniques are needed, not just for specific tech.


Final Notes

This reference provides core testing principles and patterns. For detailed implementations and complete examples, refer to the example files listed above. Each example file is self-contained and can be read independently based on your testing needs.