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
npx -y skills add CowDogMoo/squad-agents --skill go-testing-patternsAssembled 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
- Testing Philosophy
- Table-Driven Tests
- Subtests
- Test Helpers
- Mocking Strategies
- Benchmarks
- Test Coverage
- Common Patterns
- What Not to Test
- Cobra CLI Testing
Testing Philosophy
Core Principles
| Principle | Description |
|---|---|
| Simple and readable | Tests should be easy to understand at a glance |
| Test behavior | Test what the code does, not how it does it |
| Avoid over-mocking | Only mock when truly necessary |
| Clarity over DRY | Prefer clarity to avoiding repetition in tests |
| One thing per test | Each 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
| Type | Target |
|---|---|
| Unit tests | 70-80% |
| Critical paths | 90%+ |
| Edge cases | As 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.gofile. _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.Cleanupinside every subtest — restoresSetOut,SetErr, andSetArgsso the next subtest starts clean.- Fresh
bytes.Bufferper subtest — never share a buffer. - Assert on error content — check
err.Error()or stderr for the expected substring, not justerr != 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
- Go Testing Package
- Table-Driven Tests
- testify Package
- Testing in Go: Naming Conventions
- Best Practices for Test Files (Symflower)
- Google Go Style Decisions
- golang/go#33688 — Test file organization
- Organizing Go Tests — Comprehensive Guide
Last updated: 2026-01-10