690 lines
17 KiB
Markdown
690 lines
17 KiB
Markdown
# 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
|
|
|
|
```go
|
|
// ✅ 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:
|
|
|
|
```go
|
|
// ❌ 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:**
|
|
|
|
```go
|
|
// ✅ 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:
|
|
|
|
```go
|
|
// ❌ 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// ✅ External package - tests public API only
|
|
package user_test
|
|
|
|
import (
|
|
"testing"
|
|
"github.com/yourorg/project/user"
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Real Implementation Patterns
|
|
|
|
### In-Memory Repository
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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.
|