This skill should be used when writing Go tests, creating test fixtures, benchmarks, table-driven tests, mocking dependencies, or improving test coverage.
This skill defines comprehensive testing patterns for Go, covering unit tests, integration tests, benchmarks, mocking, and test organization.
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 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: "[email protected]", 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) {
// ...
})
}
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"},
}
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 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 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
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")
}
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")
}
}
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: "[email protected]",
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)
}
})
}
}
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 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
}
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)
}
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 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)
}
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
}
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
}
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: "[email protected]"}
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
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()
}
}
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
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: "[email protected]"},
{name: "normal email", input: "[email protected]"},
{name: "long email", input: "[email protected]"},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
ValidateEmail(bm.input)
}
})
}
}
Use Go's built-in fuzzing to discover edge cases.
// CORRECT: Fuzz test
func FuzzParseEmail(f *testing.F) {
// Seed corpus
f.Add("[email protected]")
f.Add("[email protected]")
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
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
})
}
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: "[email protected]"}, 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++
}
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())
}
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
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}'
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.