Loading...
Loading...
Go testing patterns for production-grade code: subtests, test helpers, fixtures, golden files, httptest, testcontainers, property-based testing, and fuzz testing. Covers mocking strategies, test isolation, coverage analysis, and test design philosophy. Use when writing tests, improving coverage, reviewing test quality, setting up test infrastructure, or choosing a testing approach. Trigger examples: "add tests", "improve coverage", "write tests for this", "test helpers", "mock this dependency", "integration test", "fuzz test". Do NOT use for performance benchmarking methodology (use go-performance-review), security testing (use go-security-audit), or table-driven test patterns specifically (use go-test-table-driven).
npx skill4agent add eduardo-sl/go-agent-skills go-test-quality// ✅ Good — tests what the function DOES
func TestTransferFunds_InsufficientBalance(t *testing.T) {
from := NewAccount("alice", 100)
to := NewAccount("bob", 0)
err := TransferFunds(from, to, 150)
require.ErrorIs(t, err, ErrInsufficientFunds)
assert.Equal(t, 100, from.Balance(), "sender balance should be unchanged")
assert.Equal(t, 0, to.Balance(), "receiver balance should be unchanged")
}
// ❌ Bad — tests HOW the function does it
func TestTransferFunds_InsufficientBalance(t *testing.T) {
// asserts that debit() was called before credit()
// asserts that rollback() was called
// asserts internal mutex was locked
}assert// ✅ Good — reads like a sentence
func TestOrderService_Cancel_RefundsPartiallyShippedItems(t *testing.T) { ... }
func TestParseConfig_ReturnsErrorOnMissingRequiredField(t *testing.T) { ... }
func TestRateLimiter_AllowsBurstAfterCooldown(t *testing.T) { ... }
// ❌ Bad — says nothing useful
func TestCancel(t *testing.T) { ... }
func TestParseConfig2(t *testing.T) { ... }
func TestRateLimiter_Success(t *testing.T) { ... }t.Runfunc TestUserService_Create(t *testing.T) {
svc := setupUserService(t)
t.Run("succeeds with valid input", func(t *testing.T) {
user, err := svc.Create(ctx, CreateUserInput{
Name: "Alice",
Email: "alice@example.com",
})
require.NoError(t, err)
assert.NotEmpty(t, user.ID)
assert.Equal(t, "Alice", user.Name)
})
t.Run("rejects duplicate email", func(t *testing.T) {
_, _ = svc.Create(ctx, CreateUserInput{
Name: "Alice", Email: "taken@example.com",
})
_, err := svc.Create(ctx, CreateUserInput{
Name: "Bob", Email: "taken@example.com",
})
require.ErrorIs(t, err, ErrDuplicateEmail)
})
t.Run("rejects empty name", func(t *testing.T) {
_, err := svc.Create(ctx, CreateUserInput{
Name: "", Email: "valid@example.com",
})
var valErr *ValidationError
require.ErrorAs(t, err, &valErr)
assert.Equal(t, "name", valErr.Field)
})
}rejects duplicate emailTestUserService_Create/case_3t.Helper()func createTestUser(t *testing.T, svc *UserService, name string) *User {
t.Helper()
user, err := svc.Create(context.Background(), CreateUserInput{
Name: name,
Email: name + "@test.com",
})
require.NoError(t, err)
return user
}func newTestOrder(t *testing.T, opts ...func(*Order)) *Order {
t.Helper()
o := &Order{
ID: uuid.New(),
UserID: uuid.New(),
Status: OrderStatusPending,
Total: 9999, // $99.99
CreatedAt: time.Now(),
}
for _, opt := range opts {
opt(o)
}
return o
}
// Usage — only override what matters for THIS test
func TestOrder_Cancel_RejectsShippedOrders(t *testing.T) {
order := newTestOrder(t, func(o *Order) {
o.Status = OrderStatusShipped
})
err := order.Cancel()
require.ErrorIs(t, err, ErrCannotCancelShipped)
}t.Cleanupt.Cleanupdefert.FailNow()func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("postgres", testDSN)
require.NoError(t, err)
t.Cleanup(func() {
db.Close()
})
return db
}var update = flag.Bool("update", false, "update golden files")
func TestRenderInvoice(t *testing.T) {
invoice := buildTestInvoice()
got, err := RenderInvoice(invoice)
require.NoError(t, err)
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
// Run: go test -update to regenerate golden files
require.NoError(t, os.WriteFile(golden, got, 0644))
}
want, err := os.ReadFile(golden)
require.NoError(t, err)
assert.Equal(t, string(want), string(got))
}testdata/go buildhttptesthttptest.NewRecorderfunc TestUserHandler_GetByID(t *testing.T) {
store := &mockUserStore{
getByIDFunc: func(ctx context.Context, id string) (*User, error) {
if id == "123" {
return &User{ID: "123", Name: "Alice"}, nil
}
return nil, ErrNotFound
},
}
handler := NewUserHandler(store, zap.NewNop())
t.Run("returns user as JSON", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
req.SetPathValue("id", "123")
rec := httptest.NewRecorder()
handler.HandleGet(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Header().Get("Content-Type"), "application/json")
var body map[string]string
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
assert.Equal(t, "Alice", body["name"])
})
t.Run("returns 404 for unknown user", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/unknown", nil)
req.SetPathValue("id", "unknown")
rec := httptest.NewRecorder()
handler.HandleGet(rec, req)
assert.Equal(t, http.StatusNotFound, rec.Code)
})
}httptest.NewServerfunc TestAPI_CreateUser_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
app := setupApp(t)
srv := httptest.NewServer(app.Router())
t.Cleanup(srv.Close)
resp, err := http.Post(srv.URL+"/api/v1/users",
"application/json",
strings.NewReader(`{"name":"Alice","email":"alice@test.com"}`))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
}type mockNotifier struct {
sendFunc func(ctx context.Context, to, msg string) error
sent []string
}
func (m *mockNotifier) Send(ctx context.Context, to, msg string) error {
m.sent = append(m.sent, to)
if m.sendFunc != nil {
return m.sendFunc(ctx, to, msg)
}
return nil
}type Service struct {
now func() time.Time
randID func() string
}
// Production: svc := &Service{now: time.Now, randID: uuid.NewString}
// Test: svc := &Service{now: fixedTime, randID: func() string { return "abc" }}json.Marshalfunc TestPostgresUserStore(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
pg, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("testdb"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready").
WithOccurrence(2).
WithStartupTimeout(30*time.Second),
),
)
require.NoError(t, err)
t.Cleanup(func() { pg.Terminate(ctx) })
connStr, err := pg.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
store, err := NewPostgresStore(connStr)
require.NoError(t, err)
t.Run("create and retrieve user", func(t *testing.T) {
created, err := store.Create(ctx, &User{Name: "Alice"})
require.NoError(t, err)
fetched, err := store.GetByID(ctx, created.ID)
require.NoError(t, err)
assert.Equal(t, "Alice", fetched.Name)
})
}//go:build integrationgo test -tags=integration -count=1 ./...func FuzzParseEmail(f *testing.F) {
f.Add("alice@example.com")
f.Add("")
f.Add("@")
f.Fuzz(func(t *testing.T, input string) {
result, err := ParseEmail(input)
if err != nil {
return // invalid input is fine, just don't panic
}
// Round-trip: parsing the output should give the same result
reparsed, err := ParseEmail(result.String())
require.NoError(t, err)
assert.Equal(t, result, reparsed)
})
}go test -fuzz=FuzzParseEmail -fuzztime=30sfunc TestSlugify(t *testing.T) {
t.Parallel()
t.Run("lowercases input", func(t *testing.T) {
t.Parallel()
assert.Equal(t, "hello-world", Slugify("Hello World"))
})
t.Run("strips special characters", func(t *testing.T) {
t.Parallel()
assert.Equal(t, "caf", Slugify("café!"))
})
}t.Parallel()os.Setenvvar testDB *sql.DB
func TestMain(m *testing.M) {
var teardown func()
testDB, teardown = setupTestDatabase()
code := m.Run()
teardown()
os.Exit(code)
}go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
go tool cover -func=coverage.outtime.SleepTest1TestSuccesst.Helper()t.Cleanup()t.Parallel()testing.Short()go test -race ./...