go-testing
Go Testing Skill
Go testing follows a 4-phase workflow: understand what needs testing, write idiomatic tests, run and verify, review quality. Every function with multiple test cases uses table-driven pattern. Every helper calls t.Helper(). Every concurrent test runs with -race.
Instructions
Phase 1: UNDERSTAND Test Requirements
Goal: Determine what needs testing and the appropriate test strategy.
Step 1: Identify test scope
- What function/method/package is being tested?
- Is this a new test, modification, or coverage gap?
- Are there existing tests to follow as patterns?
Read and follow repository CLAUDE.md before writing tests — project conventions override these defaults.
Step 2: Choose test type
| Need | Test Type | Pattern |
|---|---|---|
| Multiple input/output cases | Table-driven unit test | []struct + t.Run |
| Single specific behavior | Focused unit test | Standard TestXxx |
| Cross-component interaction | Integration test | Setup/teardown helpers |
| Performance measurement | Benchmark | b.Loop() with b.ReportAllocs() |
| API usage documentation | Example test | ExampleXxx functions |
Step 3: Verify test file structure
package mypackage_test // Black-box testing (preferred over internal)
import (
"testing"
"mymodule/mypackage"
)
// Order: Unit tests, Integration tests, Benchmarks, Examples
Black-box testing (package_test) is preferred because it tests the public API the
way consumers use it. Internal testing (package) is acceptable only when testing
unexported behavior that can't be reached through the public API.
Gate: Test scope, type, and file location identified. Proceed only when gate passes.
Phase 2: WRITE Tests
Goal: Implement tests following Go idioms.
Step 1: Table-driven tests for multiple cases
Multiple related cases MUST use table-driven pattern — this is the canonical Go testing idiom because it makes adding cases trivial and the input/output relationship explicit:
func TestParseConfig(t *testing.T) {
tests := []struct {
name string
input string
want Config
wantErr bool
}{
{
name: "valid YAML",
input: `key: value`,
want: Config{Key: "value"},
},
{
name: "invalid syntax",
input: `{{{`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseConfig(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("ParseConfig() = %v, want %v", got, tt.want)
}
})
}
}
Write only the tests needed — no speculative coverage, no "while I'm here" additions. Each test should exercise a specific behavior, not pad a coverage number.
Step 2: Test helpers with t.Helper()
Every test helper MUST call t.Helper() as its first line — without it, test
failure messages point to the helper's line instead of the caller's, which makes
debugging failures slow and frustrating:
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper() // MUST be first line
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
Step 3: Mocking interfaces
Use manual mocks with function fields for flexible per-test behavior:
type MockStore struct {
GetFunc func(key string) ([]byte, error)
}
func (m *MockStore) Get(key string) ([]byte, error) {
if m.GetFunc != nil {
return m.GetFunc(key)
}
return nil, nil
}
See references/go-test-patterns.md for complete mock patterns with call tracking.
Step 4: Parallel tests
Use t.Parallel() for independent tests by default — it catches unintended
shared state and runs faster:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// test body (use tt directly in Go 1.22+)
})
}
Step 5: Test error paths too
Test error conditions, not just happy paths. If a function can return an error, write at least one test case that triggers that error and verifies the message.
Gate: Tests follow table-driven pattern, helpers use t.Helper(), mocks use function fields. Proceed only when gate passes.
Phase 3: RUN and Verify
Goal: Execute tests and confirm correctness.
Step 1: Run tests
# Standard run with verbose output
go test -v ./path/to/package/...
# With race detector (REQUIRED for concurrent code — race conditions
# are silent until production; the -race flag catches them deterministically)
go test -race -v ./path/to/package/...
# With coverage
go test -coverprofile=coverage.out ./path/to/package/...
go tool cover -func=coverage.out
Always show actual go test output. Never summarize as "tests pass" — the user
needs to see what ran, what passed, and what the output looks like.
Step 2: Verify results
- All tests pass (show actual output)
- No race conditions detected
- Critical paths have >80% coverage
- Error paths are exercised
After editing test files, use go_diagnostics (gopls MCP) to catch errors
before running tests — faster feedback than a full go test cycle.
Step 3: Run full suite
go test ./...
Verify no regressions in other packages.
Gate: All tests pass, race detector clean, coverage adequate. Proceed only when gate passes.
Phase 4: REVIEW Test Quality
Goal: Ensure tests are maintainable and complete.
Checklist:
- Table-driven tests for multiple cases?
- Test helpers marked with
t.Helper()? - Parallel execution where safe?
- Error conditions tested?
- Edge cases covered (empty, nil, boundary)?
- Cleanup performed (
t.Cleanupordefer)? - No test interdependencies?
- Race detector passes?
Test names must accurately describe the code path exercised. If a test simulates behavior rather than exercising the production code path, the name must reflect this — "pragmatic approximation" is not a valid reason for a misleading test name.
Gate: All checklist items satisfied. Tests are complete.
Benchmark Guide
Use b.Loop() (Go 1.24+) for all new benchmarks — it prevents dead code
elimination, manages timers automatically, and produces more accurate results
than the manual for i := 0; i < b.N; i++ loop:
func BenchmarkProcess(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
_ = Process(input)
}
}
Sub-benchmarks for comparison:
func BenchmarkBuilder(b *testing.B) {
b.Run("strings.Builder", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() { /* ... */ }
})
b.Run("bytes.Buffer", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() { /* ... */ }
})
}
Run: go test -bench=. -benchmem ./...
See references/go-benchmark-and-concurrency.md for benchstat comparison workflow and synctest patterns.
Commands Reference
go test ./... # Run all tests
go test -v ./... # Verbose output
go test -race ./... # Race detector
go test -run TestMyFunc ./... # Specific test
go test -run TestMyFunc/subtest ./... # Specific subtest
go test -coverprofile=coverage.out ./... # Coverage profile
go tool cover -func=coverage.out # Coverage summary
go tool cover -html=coverage.out # Coverage HTML
go test -bench=. -benchmem ./... # Benchmarks
go test -short ./... # Skip long tests
go test -timeout 30s ./... # With timeout
go test -count=10 ./... # Detect flaky tests
Error Handling
Error: "test passes but shouldn't"
Cause: Test assertion is wrong or testing the wrong thing (tautological test) Solution:
- Verify test actually exercises the code under test
- Temporarily break the implementation and confirm test fails
- Check assertion compares meaningful values, not self-referential ones
Error: "race detected during test"
Cause: Shared mutable state accessed from goroutines without synchronization Solution:
- Use
sync.Mutexoratomicoperations for shared state - Use channels for goroutine communication
- Ensure mock call tracking uses mutex protection
- See
references/go-benchmark-and-concurrency.mdfor patterns
Error: "test passes locally but fails in CI"
Cause: Environment dependency, timing assumption, or file path difference Solution:
- Use
t.TempDir()instead of hardcoded paths - Use
t.Setenv()for environment variables (auto-restored) - Replace
time.Sleepwithsynctest.Testfor timing-dependent tests - Check for platform-specific assumptions
Available Scripts
scripts/gen-table-test.sh— Scaffold a table-driven test file for a Go functionscripts/bench-compare.sh— Run Go benchmarks with optional benchstat comparison
References
${CLAUDE_SKILL_DIR}/references/go-test-patterns.md: Full examples for table-driven tests, helpers, mocking, interface deduplication${CLAUDE_SKILL_DIR}/references/go-benchmark-and-concurrency.md: b.Loop() benchmarks, benchstat, synctest, race detection patterns
More from notque/claude-code-toolkit
generate-claudemd
Generate project-specific CLAUDE.md from repo analysis.
12fish-shell-config
Fish shell configuration and PATH management.
12pptx-generator
PPTX presentation generation with visual QA: slides, pitch decks.
12codebase-overview
Systematic codebase exploration and architecture mapping.
10image-to-video
FFmpeg-based video creation from image and audio.
9data-analysis
Decision-first data analysis with statistical rigor gates.
9