golang-testing-patterns · diff
git:20260205.6d9a0d7 to git:20260304.7bde6ac
2 added, 2 removed. Audit A to A.
---
name: golang-testing-patterns
description: Modern Go testing patterns including table-driven tests, subtests, test organization, and best practices. Use when writing or refactoring tests, implementing test coverage, or when the user asks about Go testing approaches.
---
# Go Testing Patterns
Modern testing patterns for Go following 2025-2026 best practices.
## Table-Driven Tests
The idiomatic Go approach for comprehensive testing.
**Basic structure:**
```go
func TestFeature(t *testing.T) {
tests := []struct {
name string
input string
expected int
wantErr bool
}{
{
name: "valid input",
input: "hello",
expected: 5,
wantErr: false,
},
{
name: "empty input",
input: "",
expected: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessString(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.expected {
t.Errorf("got %d, want %d", got, tt.expected)
}
})
}
}
```
## Using Subtests with t.Run()
**Benefits:**
- Clear failure messages showing which case failed
- Can run specific tests: `go test -run TestFeature/valid_input`
- Parallel execution support
**Pattern:**
```go
for _, tt := range tests {
- tt := tt // Required for parallel tests
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Run subtests in parallel
// ... test logic
})
}
```
+
## Test Organization
**File structure:**
- Tests live alongside code: `service.go` → `service_test.go`
- Integration tests in separate package: `service_integration_test.go`
- Shared test utilities: `test/util/` or `testutil/`
**Test function naming:**
```go
// Unit tests
func TestSessionService_CreateSession(t *testing.T) {}
func TestSessionService_CreateSession_ValidationError(t *testing.T) {}
// Integration tests
func TestSessionService_Integration(t *testing.T) {}
```
## Setup and Teardown
**Using t.Cleanup():**
```go
func TestWithCleanup(t *testing.T) {
// Setup
db := setupTestDB(t)
t.Cleanup(func() {
db.Close() // Always runs, even if test fails
})
// Test logic
// ...
}
```
**Setup once for all subtests:**
```go
func TestSuite(t *testing.T) {
// Shared setup
db := setupTestDB(t)
t.Cleanup(func() { db.Close() })
t.Run("test1", func(t *testing.T) {
// Uses shared db
})
t.Run("test2", func(t *testing.T) {
// Uses shared db
})
}
```
## Testing Database Operations
**Transaction-based isolation:**
```go
func TestDatabaseOperation(t *testing.T) {
db := setupTestDB(t)
tests := []struct {
name string
// ... test fields
}{
// ... test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Each subtest gets fresh data
tx, _ := db.Begin()
t.Cleanup(func() { tx.Rollback() })
// Use tx for test operations
// ...
})
}
}
```
## Error Testing Patterns
**Check error occurrence:**
```go
if tt.wantErr {
if err == nil {
t.Errorf("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
```
**Check specific error:**
```go
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got %v", err)
}
```
**Check error message:**
```go
if err == nil || !strings.Contains(err.Error(), "required") {
t.Errorf("expected validation error, got %v", err)
}
```
## Test Helpers
**Creating test data:**
```go
func newTestSession(t *testing.T, overrides ...func(*ent.AlertSession)) *ent.AlertSession {
t.Helper() // Ensures stack traces point to the caller
session := &ent.AlertSession{
ID: "test-123",
Status: "pending",
StartedAt: time.Now(),
}
for _, override := range overrides {
override(session)
}
return session
}
// Usage
session := newTestSession(t, func(s *ent.AlertSession) {
s.Status = "completed"
})
```
## Mocking and Interfaces
**Interface-based testing:**
```go
type SessionRepository interface {
GetByID(ctx context.Context, id string) (*Session, error)
}
// In test
type mockSessionRepo struct {
sessions map[string]*Session
}
func (m *mockSessionRepo) GetByID(ctx context.Context, id string) (*Session, error) {
s, ok := m.sessions[id]
if !ok {
return nil, ErrNotFound
}
return s, nil
}
```
## Testing Patterns for TARSy
**Service layer tests:**
```go
func TestSessionService_CreateSession(t *testing.T) {
// Setup test database with cleanup
client, _ := test.SetupTestDatabase(t)
service := services.NewSessionService(client)
tests := []struct {
name string
req models.CreateSessionRequest
wantErr bool
errMsg string
}{
{
name: "valid session",
req: models.CreateSessionRequest{
SessionID: "sess-123",
// ... other fields
},
wantErr: false,
},
{
name: "missing session ID",
req: models.CreateSessionRequest{
SessionID: "",
},
wantErr: true,
errMsg: "session_id",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- ctx := context.Background()
+ ctx := t.Context()
session, err := service.CreateSession(ctx, tt.req)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("error %q doesn't contain %q", err.Error(), tt.errMsg)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if session.ID != tt.req.SessionID {
t.Errorf("session ID = %q, want %q", session.ID, tt.req.SessionID)
}
})
}
}
```
## Quick Reference
**When to use table-driven tests:**
- Multiple similar test cases with different inputs/outputs
- Testing edge cases and validation
- Any test with more than 2-3 cases
**When to skip table-driven tests:**
- Single, unique test case
- Complex setup that varies significantly between cases
- Integration tests with sequential dependencies
**Parallel testing:**
```go
t.Parallel() // Use for fast, independent tests
// Don't use for: database tests, shared state, rate-limited APIs
```
**Assertions:**
- Use `t.Errorf()` for non-fatal assertions
- Use `t.Fatalf()` when continuing would panic or is meaningless
- Always include both `got` and `want` in error messages