Go’s testing package is minimal by design — table-driven tests, benchmarks, and fuzzing are idiomatic patterns the language encourages. testify adds assertion helpers and mock generation. testcontainers-go spins up real PostgreSQL, Redis, or Kafka instances in Docker for integration tests without test doubles. Claude Code generates table-driven test suites, mock implementations, HTTP handler tests, and the testcontainers setup that makes database tests reliable.
CLAUDE.md for Go Testing
## Testing Standards
- Unit tests: table-driven with t.Run() — all new functions have table tests
- Assertion library: testify (assert/require — use require to stop on first failure)
- Mocks: mockery v2 for interface mocks — run `go generate ./...` to regenerate
- HTTP tests: httptest.NewRecorder() + httptest.NewServer() for handler tests
- Integration: testcontainers-go with PostgreSQL and Redis images
- Benchmarks: b.ResetTimer() after setup, b.RunParallel() for concurrent benchmarks
- Fuzz: fuzz targets for parsers, serializers, input validators
- Coverage: minimum 80% for packages in internal/; aim for 90% on critical paths
- Test database: separate from dev/prod — isolated per test with t.Cleanup()
Table-Driven Unit Tests
// internal/orders/service_test.go
package orders_test
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/myapp/internal/orders"
"github.com/myapp/internal/orders/mocks"
)
func TestOrderService_Create(t *testing.T) {
t.Parallel()
type args struct {
customerID string
items []orders.OrderItem
}
tests := []struct {
name string
args args
setupMock func(*mocks.OrderRepository, *mocks.InventoryService)
want *orders.Order
wantErr bool
wantErrType error
}{
{
name: "creates order successfully",
args: args{
customerID: "cust_123",
items: []orders.OrderItem{{ProductID: "prod_abc", Qty: 2}},
},
setupMock: func(repo *mocks.OrderRepository, inv *mocks.InventoryService) {
inv.On("CheckStock", "prod_abc", 2).Return(true, nil)
repo.On("Create", mock.MatchedBy(func(o *orders.Order) bool {
return o.CustomerID == "cust_123" && len(o.Items) == 1
})).Return(&orders.Order{
ID: "ord_xyz", CustomerID: "cust_123", Status: "pending",
}, nil)
},
want: &orders.Order{ID: "ord_xyz", CustomerID: "cust_123", Status: "pending"},
wantErr: false,
},
{
name: "returns error when item out of stock",
args: args{
customerID: "cust_123",
items: []orders.OrderItem{{ProductID: "prod_abc", Qty: 100}},
},
setupMock: func(repo *mocks.OrderRepository, inv *mocks.InventoryService) {
inv.On("CheckStock", "prod_abc", 100).Return(false, nil)
},
wantErr: true,
wantErrType: orders.ErrInsufficientStock,
},
{
name: "returns error on empty items",
args: args{
customerID: "cust_123",
items: []orders.OrderItem{},
},
setupMock: func(_ *mocks.OrderRepository, _ *mocks.InventoryService) {},
wantErr: true,
wantErrType: orders.ErrEmptyOrder,
},
{
name: "wraps repository error",
args: args{
customerID: "cust_123",
items: []orders.OrderItem{{ProductID: "prod_abc", Qty: 1}},
},
setupMock: func(repo *mocks.OrderRepository, inv *mocks.InventoryService) {
inv.On("CheckStock", "prod_abc", 1).Return(true, nil)
repo.On("Create", mock.Anything).Return(nil, errors.New("connection refused"))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
repo := mocks.NewOrderRepository(t)
inv := mocks.NewInventoryService(t)
tt.setupMock(repo, inv)
svc := orders.NewService(repo, inv)
got, err := svc.Create(context.Background(), tt.args.customerID, tt.args.items)
if tt.wantErr {
require.Error(t, err)
if tt.wantErrType != nil {
assert.ErrorIs(t, err, tt.wantErrType)
}
return
}
require.NoError(t, err)
assert.Equal(t, tt.want.ID, got.ID)
assert.Equal(t, tt.want.CustomerID, got.CustomerID)
})
}
}
Interface Mock with mockery
// internal/orders/repository.go — define the interface
package orders
type OrderRepository interface {
Create(ctx context.Context, order *Order) (*Order, error)
FindByID(ctx context.Context, id string) (*Order, error)
Update(ctx context.Context, order *Order) error
ListByCustomer(ctx context.Context, customerID string, limit, offset int) ([]*Order, int64, error)
}
//go:generate mockery --name=OrderRepository --output=./mocks --outpkg=mocks --with-expecter
// internal/orders/mocks/OrderRepository.go — generated by mockery but shown for clarity
// Usage pattern in tests:
func TestWithMockery(t *testing.T) {
repo := mocks.NewOrderRepository(t)
// Type-safe expecter API (mockery v2 feature)
repo.EXPECT().
FindByID(mock.Anything, "ord_123").
Return(&Order{ID: "ord_123"}, nil).
Once()
// Or fluent style
repo.On("FindByID", mock.Anything, "ord_123").
Return(&Order{ID: "ord_123"}, nil)
}
HTTP Handler Tests
// internal/api/handlers/orders_test.go
package handlers_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateOrderHandler(t *testing.T) {
t.Parallel()
tests := []struct {
name string
body any
setupMock func(*mocks.OrderService)
wantStatusCode int
wantBody map[string]any
}{
{
name: "201 on valid request",
body: map[string]any{
"customer_id": "cust_123",
"items": []map[string]any{
{"product_id": "prod_abc", "qty": 2},
},
},
setupMock: func(svc *mocks.OrderService) {
svc.On("Create", mock.Anything, "cust_123", mock.Anything).
Return(&orders.Order{ID: "ord_xyz", Status: "pending"}, nil)
},
wantStatusCode: http.StatusCreated,
wantBody: map[string]any{"id": "ord_xyz", "status": "pending"},
},
{
name: "400 on missing customer_id",
body: map[string]any{
"items": []map[string]any{{"product_id": "prod_abc", "qty": 1}},
},
setupMock: func(_ *mocks.OrderService) {},
wantStatusCode: http.StatusBadRequest,
},
{
name: "409 on insufficient stock",
body: map[string]any{
"customer_id": "cust_123",
"items": []map[string]any{{"product_id": "prod_abc", "qty": 1000}},
},
setupMock: func(svc *mocks.OrderService) {
svc.On("Create", mock.Anything, mock.Anything, mock.Anything).
Return(nil, orders.ErrInsufficientStock)
},
wantStatusCode: http.StatusConflict,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
svc := mocks.NewOrderService(t)
tt.setupMock(svc)
handler := handlers.NewOrderHandler(svc)
bodyBytes, err := json.Marshal(tt.body)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/orders", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler.Create(rr, req)
assert.Equal(t, tt.wantStatusCode, rr.Code)
if tt.wantBody != nil {
var gotBody map[string]any
require.NoError(t, json.NewDecoder(rr.Body).Decode(&gotBody))
for k, v := range tt.wantBody {
assert.Equal(t, v, gotBody[k])
}
}
})
}
}
Integration Tests with testcontainers
// internal/orders/repository_integration_test.go
//go:build integration
package orders_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
ctx := context.Background()
container, err := postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:16-alpine"),
postgres.WithDatabase("testdb"),
postgres.WithUsername("testuser"),
postgres.WithPassword("testpass"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(30 * time.Second),
),
)
require.NoError(t, err)
t.Cleanup(func() { container.Terminate(ctx) })
connStr, err := container.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
db, err := sql.Open("pgx", connStr)
require.NoError(t, err)
// Run migrations
require.NoError(t, runMigrations(db))
return db
}
func TestOrderRepository_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
db := setupTestDB(t)
repo := orders.NewPostgresRepository(db)
ctx := context.Background()
t.Run("creates and retrieves order", func(t *testing.T) {
order := &orders.Order{
CustomerID: "cust_123",
Status: "pending",
TotalCents: 4999,
}
created, err := repo.Create(ctx, order)
require.NoError(t, err)
require.NotEmpty(t, created.ID)
retrieved, err := repo.FindByID(ctx, created.ID)
require.NoError(t, err)
assert.Equal(t, created.ID, retrieved.ID)
assert.Equal(t, "pending", retrieved.Status)
})
}
Benchmarks
// internal/orders/pricing_bench_test.go
func BenchmarkCalculateTotals(b *testing.B) {
items := make([]OrderItem, 50)
for i := range items {
items[i] = OrderItem{Price: 999, Qty: i%10 + 1}
}
b.ResetTimer() // Don't count setup time
b.ReportAllocs()
for b.Loop() { // Go 1.24+ idiom (replaces for i := 0; i < b.N; i++)
_ = calculateTotals(items)
}
}
func BenchmarkCalculateTotals_Parallel(b *testing.B) {
items := generateItems(50)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = calculateTotals(items)
}
})
}
Fuzz Testing
// internal/orders/parser_fuzz_test.go
func FuzzParseOrderID(f *testing.F) {
// Seed corpus
f.Add("ord_abc123")
f.Add("ord_")
f.Add("")
f.Add("ord_" + strings.Repeat("a", 1000))
f.Fuzz(func(t *testing.T, input string) {
// Must not panic — all inputs are valid (return error, don't panic)
result, err := ParseOrderID(input)
if err == nil {
// If parsed successfully, re-serializing must produce the same ID
assert.Equal(t, input, result.String())
}
// err != nil is fine — just must not panic
})
}
For the Go microservices that these tests validate, see the Go microservices guide for service architecture and gRPC patterns. For the CI pipeline that runs unit tests, integration tests, and benchmarks automatically, the CI/CD advanced guide covers GitHub Actions workflow configuration. The Claude Skills 360 bundle includes Go testing skill sets covering table tests, mockery mocks, testcontainers integration, and fuzz testing. Start with the free tier to try Go test generation.