npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-golangWant just this skill?
Then install: npx claudepluginhub u/[userId]/[slug]
This skill should be used when writing Go tests, creating test fixtures, benchmarks, table-driven tests, mocking dependencies, or improving test coverage.
This skill uses the workspace's default tool permissions.
Go Testing Patterns and Best Practices
This skill defines comprehensive testing patterns for Go, covering unit tests, integration tests, benchmarks, mocking, and test organization.
Table-Driven Tests
Default Testing Pattern
Table-driven tests are the standard pattern for Go testing. They reduce duplication and make it easy to add new test cases.
// CORRECT: Table-driven test pattern
func TestAdd(t *testing.T) {
tests := []struct {
name string
a int
b int
expected int
}{
{name: "positive numbers", a: 2, b: 3, expected: 5},
{name: "negative numbers", a: -2, b: -3, expected: -5},
{name: "mixed signs", a: -2, b: 3, expected: 1},
{name: "zero values", a: 0, b: 0, expected: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d, expected %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
// WRONG: Repetitive individual tests
func TestAddPositive(t *testing.T) {
got := Add(2, 3)
if got != 5 {
t.Errorf("expected 5, got %d", got)
}
}
func TestAddNegative(t *testing.T) {
got := Add(-2, -3)
if got != -5 {
t.Errorf("expected -5, got %d", got)
}
}
// More duplication...
Use tt Loop Variable
Use tt as the conventional name for the table test variable.
// CORRECT: Use tt for test case variable
func TestValidate(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{name: "valid input", input: "test@example.com", wantErr: false},
{name: "invalid input", input: "not-an-email", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Validate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
// WRONG: Inconsistent variable naming
for _, tc := range tests { // Use tt, not tc or testCase
t.Run(tc.name, func(t *testing.T) {
// ...
})
}
Descriptive Test Names
Use clear, descriptive names for test cases that explain what is being tested.
// CORRECT: Descriptive test case names
tests := []struct {
name string
// ...
}{
{name: "empty input returns error"},
{name: "valid email passes validation"},
{name: "email without @ symbol fails"},
{name: "concurrent access is thread-safe"},
}
// WRONG: Unclear test case names
tests := []struct {
name string
// ...
}{
{name: "test1"},
{name: "case2"},
{name: "good"},
{name: "bad"},
}
Always Use t.Run for Subtests
Use t.Run to create subtests for better test organization and parallel execution.
// CORRECT: t.Run for subtests
func TestUserService(t *testing.T) {
tests := []struct {
name string
id string
want *User
}{
{name: "existing user", id: "123", want: &User{ID: "123", Name: "Alice"}},
{name: "non-existent user", id: "999", want: nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := service.GetUser(tt.id)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetUser() = %v, want %v", got, tt.want)
}
})
}
}
// WRONG: No subtests, harder to identify failures
func TestUserService(t *testing.T) {
tests := []struct {
id string
want *User
}{
{id: "123", want: &User{ID: "123"}},
{id: "999", want: nil},
}
for _, tt := range tests {
got := service.GetUser(tt.id) // Can't tell which case failed
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("failed for %s", tt.id)
}
}
}
Test File Naming and Organization
Use _test.go Suffix
Test files must end with _test.go and be in the same package or _test package.
// user.go
package user
type User struct {
ID string
Name string
}
// CORRECT: user_test.go (same package)
package user
import "testing"
func TestNewUser(t *testing.T) {
u := NewUser("1", "Alice")
if u.ID != "1" {
t.Errorf("expected ID 1, got %s", u.ID)
}
}
// CORRECT: user_test.go (external test package)
package user_test
import (
"testing"
"myapp/user"
)
func TestUserAPI(t *testing.T) {
u := user.NewUser("1", "Alice")
// Test exported API only
}
Test Function Naming Convention
Test functions must start with Test, benchmarks with Benchmark, examples with Example.
// CORRECT: Test function naming
func TestUserValidation(t *testing.T) {}
func TestUser_SetName(t *testing.T) {}
func TestUserService_GetUser_NotFound(t *testing.T) {}
func BenchmarkUserValidation(b *testing.B) {}
func BenchmarkUser_SetName(b *testing.B) {}
func ExampleUser_SetName() {}
// WRONG: Invalid function names
func userValidation(t *testing.T) {} // Must start with Test
func Test_user_validation(t *testing.T) {} // Use TestUserValidation
func testUserValidation(t *testing.T) {} // Must start with capital T
testify Package Usage
require vs assert
Use require for critical assertions that should stop the test immediately. Use assert for non-critical checks.
// CORRECT: require for critical setup, assert for checks
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserService(t *testing.T) {
db, err := setupTestDB()
require.NoError(t, err, "failed to setup test DB") // Stop if setup fails
require.NotNil(t, db)
svc := NewService(db)
user, err := svc.GetUser("123")
require.NoError(t, err)
assert.Equal(t, "123", user.ID) // Continue even if fails
assert.Equal(t, "Alice", user.Name) // Can check multiple fields
assert.True(t, user.Active)
}
// WRONG: Using assert for critical setup
func TestUserService(t *testing.T) {
db, err := setupTestDB()
assert.NoError(t, err) // Test continues even if DB setup failed!
svc := NewService(db) // nil pointer panic if db is nil
user, err := svc.GetUser("123")
}
require.NoError for Error Checks
Use require.NoError for clear error checking in tests.
// CORRECT: require.NoError
func TestLoadConfig(t *testing.T) {
cfg, err := LoadConfig("config.json")
require.NoError(t, err, "LoadConfig should not return error")
require.NotNil(t, cfg)
assert.Equal(t, "localhost", cfg.Host)
assert.Equal(t, 8080, cfg.Port)
}
// WRONG: Manual error checking in tests
func TestLoadConfig(t *testing.T) {
cfg, err := LoadConfig("config.json")
if err != nil {
t.Fatalf("unexpected error: %v", err) // Verbose
}
if cfg == nil {
t.Fatal("cfg should not be nil")
}
}
testify Table Tests
Combine table-driven tests with testify assertions.
// CORRECT: Table tests with testify
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
errMsg string
}{
{
name: "valid email",
email: "user@example.com",
wantErr: false,
},
{
name: "missing @ symbol",
email: "userexample.com",
wantErr: true,
errMsg: "invalid email format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
} else {
require.NoError(t, err)
}
})
}
}
HTTP Testing
Use httptest.NewServer for Integration Tests
Use httptest.NewServer to create a test HTTP server for integration testing.
// CORRECT: httptest.NewServer for integration tests
func TestAPIClient_GetUser(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/users/123", r.URL.Path)
assert.Equal(t, "GET", r.Method)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"id": "123",
"name": "Alice",
})
}))
defer server.Close()
client := NewClient(server.URL)
user, err := client.GetUser("123")
require.NoError(t, err)
assert.Equal(t, "123", user.ID)
assert.Equal(t, "Alice", user.Name)
}
Use httptest.NewRecorder for Unit Tests
Use httptest.NewRecorder to test HTTP handlers without starting a server.
// CORRECT: httptest.NewRecorder for handler unit tests
func TestGetUserHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler := GetUserHandler(mockUserService)
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response map[string]string
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.Equal(t, "123", response["id"])
}
// WRONG: Creating actual server for unit tests
func TestGetUserHandler(t *testing.T) {
server := httptest.NewServer(GetUserHandler(mockUserService)) // Overkill
defer server.Close()
resp, err := http.Get(server.URL + "/users/123")
// Unnecessary complexity for unit test
}
Mocking with gomock
Generate Mocks with mockgen
Use mockgen to generate mocks for interfaces. Store mocks in internal/mocks or package_test.go.
# Install mockgen
go install go.uber.org/mock/mockgen@latest
# Generate mocks
mockgen -source=user.go -destination=internal/mocks/user_mock.go -package=mocks
// user.go - Interface to mock
package user
import "context"
type Repository interface {
GetUser(ctx context.Context, id string) (*User, error)
SaveUser(ctx context.Context, user *User) error
}
// CORRECT: Using generated mocks
package user_test
import (
"context"
"testing"
"myapp/internal/mocks"
"myapp/user"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserService_GetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
mockRepo.EXPECT().
GetUser(gomock.Any(), "123").
Return(&user.User{ID: "123", Name: "Alice"}, nil)
svc := user.NewService(mockRepo)
u, err := svc.GetUser(context.Background(), "123")
require.NoError(t, err)
assert.Equal(t, "123", u.ID)
}
Store Mocks in internal/mocks Directory
Keep generated mocks organized in internal/mocks/ directory.
myapp/
internal/
mocks/
user_mock.go
payment_mock.go
user/
user.go
user_test.go
// go:generate directive in source file
//go:generate mockgen -source=user.go -destination=../internal/mocks/user_mock.go -package=mocks
package user
Use gomock.InOrder for Sequential Expectations
Use gomock.InOrder when the order of method calls matters.
// CORRECT: InOrder for sequential calls
func TestUserService_CreateAndNotify(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
mockNotifier := mocks.NewMockNotifier(ctrl)
gomock.InOrder(
mockRepo.EXPECT().SaveUser(gomock.Any(), gomock.Any()).Return(nil),
mockNotifier.EXPECT().SendWelcome(gomock.Any(), gomock.Any()).Return(nil),
)
svc := user.NewService(mockRepo, mockNotifier)
err := svc.CreateUser(context.Background(), &user.User{Name: "Alice"})
require.NoError(t, err)
}
Test Helpers
Mark Test Helpers with t.Helper
Use t.Helper() in test helper functions to report errors at the caller location.
// CORRECT: Test helper with t.Helper()
func setupTestDB(t *testing.T) *sql.DB {
t.Helper() // Errors reported in calling test, not here
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open test DB: %v", err)
}
if err := runMigrations(db); err != nil {
t.Fatalf("failed to run migrations: %v", err)
}
return db
}
func TestUserRepository(t *testing.T) {
db := setupTestDB(t) // Error reported here if setup fails
defer db.Close()
// Test implementation
}
// WRONG: Helper without t.Helper()
func setupTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed: %v", err) // Error line points here, not caller
}
return db
}
Cleanup with t.Cleanup
Use t.Cleanup for test cleanup instead of defer when cleanup depends on test context.
// CORRECT: Using t.Cleanup
func TestWithTempFile(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
require.NoError(t, err)
t.Cleanup(func() {
os.Remove(tmpfile.Name())
})
// Test using tmpfile
// Cleanup runs even if test fails
}
Benchmarks
Benchmark Function Naming
Benchmark functions must start with Benchmark and take *testing.B parameter.
// CORRECT: Benchmark naming
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
func BenchmarkUserValidation(b *testing.B) {
user := &User{ID: "123", Email: "test@example.com"}
b.ResetTimer() // Reset after setup
for i := 0; i < b.N; i++ {
user.Validate()
}
}
// WRONG: Invalid benchmark naming
func benchmarkAdd(b *testing.B) {} // Must start with capital B
func TestBenchmarkAdd(b *testing.B) {} // Don't mix Test and Benchmark
Use b.ResetTimer After Setup
Call b.ResetTimer() after expensive setup to exclude setup time from benchmark.
// CORRECT: ResetTimer after setup
func BenchmarkDatabaseQuery(b *testing.B) {
db := setupTestDB(b)
defer db.Close()
b.ResetTimer() // Don't include setup time
for i := 0; i < b.N; i++ {
db.Query("SELECT * FROM users WHERE id = ?", i)
}
}
// WRONG: Including setup in benchmark
func BenchmarkDatabaseQuery(b *testing.B) {
for i := 0; i < b.N; i++ {
db := setupTestDB(b) // Setup repeated b.N times!
db.Query("SELECT * FROM users WHERE id = ?", i)
db.Close()
}
}
Report Allocations with b.ReportAllocs
Use b.ReportAllocs() to track memory allocations in benchmarks.
// CORRECT: Report allocations
func BenchmarkStringConcat(b *testing.B) {
b.ReportAllocs() // Shows allocs/op in results
for i := 0; i < b.N; i++ {
s := ""
for j := 0; j < 100; j++ {
s += "a"
}
}
}
func BenchmarkStringBuilder(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("a")
}
_ = sb.String()
}
}
# Run benchmarks with memory stats
go test -bench=. -benchmem
# Compare benchmarks
go test -bench=. -benchmem -count=5 | tee old.txt
# Make changes
go test -bench=. -benchmem -count=5 | tee new.txt
benchstat old.txt new.txt
Table-Driven Benchmarks
Use table-driven pattern for benchmarks with multiple scenarios.
// CORRECT: Table-driven benchmarks
func BenchmarkValidation(b *testing.B) {
benchmarks := []struct {
name string
input string
}{
{name: "short email", input: "a@b.c"},
{name: "normal email", input: "user@example.com"},
{name: "long email", input: "very.long.email.address@subdomain.example.com"},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
ValidateEmail(bm.input)
}
})
}
}
Fuzzing
Use f.Fuzz for Fuzz Testing
Use Go's built-in fuzzing to discover edge cases.
// CORRECT: Fuzz test
func FuzzParseEmail(f *testing.F) {
// Seed corpus
f.Add("user@example.com")
f.Add("test@test.org")
f.Add("invalid")
f.Fuzz(func(t *testing.T, email string) {
result, err := ParseEmail(email)
// Invariants that should always hold
if err == nil {
require.NotEmpty(t, result.User)
require.NotEmpty(t, result.Domain)
require.Contains(t, email, "@")
}
})
}
# Run fuzz tests
go test -fuzz=FuzzParseEmail -fuzztime=30s
# Run with seed corpus only
go test -fuzz=FuzzParseEmail -fuzztime=0s
Seed Corpus for Fuzzing
Provide good seed inputs to guide fuzzing toward interesting cases.
// CORRECT: Good seed corpus
func FuzzJSONParse(f *testing.F) {
f.Add(`{"name":"Alice","age":30}`)
f.Add(`{"name":"Bob"}`)
f.Add(`{}`)
f.Add(`{"nested":{"value":true}}`)
f.Add(`[]`) // Invalid but interesting
f.Fuzz(func(t *testing.T, data string) {
var v interface{}
_ = json.Unmarshal([]byte(data), &v)
// Should not panic
})
}
Parallel Tests
Use t.Parallel for Independent Tests
Mark independent tests as parallel to speed up test execution.
// CORRECT: Parallel tests
func TestUserValidation(t *testing.T) {
t.Parallel() // Can run in parallel with other tests
tests := []struct {
name string
user *User
want error
}{
{name: "valid user", user: &User{ID: "1", Email: "a@b.c"}, want: nil},
{name: "missing email", user: &User{ID: "1"}, want: ErrInvalidEmail},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Subtests also parallel
got := tt.user.Validate()
assert.Equal(t, tt.want, got)
})
}
}
// WRONG: t.Parallel() on tests that share state
func TestSharedState(t *testing.T) {
t.Parallel() // Don't use if test modifies global state
globalCounter = 0 // Race condition!
globalCounter++
}
Golden Files
Use testdata Directory for Test Fixtures
Store test fixtures and golden files in testdata/ directory.
myapp/
parser/
parser.go
parser_test.go
testdata/
valid_input.json
invalid_input.json
expected_output.golden
// CORRECT: Using golden files
func TestParser(t *testing.T) {
input, err := os.ReadFile("testdata/valid_input.json")
require.NoError(t, err)
got, err := Parse(input)
require.NoError(t, err)
golden, err := os.ReadFile("testdata/expected_output.golden")
require.NoError(t, err)
assert.Equal(t, string(golden), got.String())
}
Update Golden Files with Flag
Provide a flag to update golden files when output changes intentionally.
// CORRECT: Golden file update flag
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
got := Render(data)
goldenPath := "testdata/output.golden"
if *update {
err := os.WriteFile(goldenPath, []byte(got), 0644)
require.NoError(t, err)
}
golden, err := os.ReadFile(goldenPath)
require.NoError(t, err)
assert.Equal(t, string(golden), got)
}
# Update golden files
go test -update
# Normal test run
go test
Coverage
Run Tests with Coverage
Always measure test coverage and aim for meaningful coverage.
# Run tests with coverage
go test -v -race -coverprofile=coverage.out ./...
# View coverage in terminal
go tool cover -func=coverage.out
# View coverage in browser
go tool cover -html=coverage.out
# Coverage by package
go test -coverprofile=coverage.out ./... && go tool cover -func=coverage.out | grep total
# Minimum coverage check in CI
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
awk '{if ($1 < 80) exit 1}'
Focus on Meaningful Coverage
Aim for high coverage of business logic, not just line coverage.
// CORRECT: Test important paths and edge cases
func TestProcessPayment(t *testing.T) {
tests := []struct {
name string
amount float64
balance float64
wantErr error
}{
{name: "sufficient balance", amount: 50, balance: 100, wantErr: nil},
{name: "insufficient balance", amount: 150, balance: 100, wantErr: ErrInsufficientFunds},
{name: "zero amount", amount: 0, balance: 100, wantErr: ErrInvalidAmount},
{name: "negative amount", amount: -50, balance: 100, wantErr: ErrInvalidAmount},
}
// Test all important scenarios
}
This skill ensures comprehensive testing coverage following Go community best practices. Apply these patterns consistently to maintain high-quality, reliable code.
Similar Skills
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.