Testing
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/go-backend-engineer/testing.md |
| Description | Not specified |
Source Content
Testing
testify for assertions/mocks, testcontainers-go for integration tests against real Postgres, zaptest for in-test logging. Always run with -race and -v.
Table-driven unit tests
func TestUserService_Create(t *testing.T) { tests := []struct { name string req *CreateUserRequest setupMock func(*mocks.MockUserRepository) wantErr bool expectedError error }{ { name: "successful user creation", req: &CreateUserRequest{Email: "test@example.com", Name: "Test", Role: "member", TenantID: "t1"}, setupMock: func(m *mocks.MockUserRepository) { m.On("GetByEmail", mock.Anything, "test@example.com", "t1"). Return(nil, domain.ErrNotFound) m.On("Create", mock.Anything, mock.AnythingOfType("*domain.User")).Return(nil) }, }, { name: "duplicate email", req: &CreateUserRequest{Email: "exists@example.com", TenantID: "t1"}, setupMock: func(m *mocks.MockUserRepository) { m.On("GetByEmail", mock.Anything, "exists@example.com", "t1"). Return(&domain.User{Email: "exists@example.com"}, nil) }, wantErr: true, expectedError: domain.ErrEmailAlreadyExists, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logger := zaptest.NewLogger(t) mockRepo := mocks.NewMockUserRepository(t) tt.setupMock(mockRepo) svc := NewUserService(mockRepo) ctx := telemetry.WithFields(context.Background(), zap.String("test", tt.name)) _ = logger user, err := svc.Create(ctx, tt.req) if tt.wantErr { require.Error(t, err) if tt.expectedError != nil { assert.ErrorIs(t, err, tt.expectedError) } return } require.NoError(t, err) require.NotNil(t, user) mockRepo.AssertExpectations(t) }) }}Mocks via mockery
task gen:mocks# runs: mockery --dir=internal/repository --all --output=test/mocks --outpkg=mocksCommit generated mocks to git so test runs don’t depend on tool installation.
Integration tests with testcontainers-go
//go:build integration
func TestUserRepository_Integration(t *testing.T) { ctx := context.Background() pg, err := postgres.RunContainer(ctx, testcontainers.WithImage("postgres:16-alpine"), postgres.WithDatabase("test"), postgres.WithUsername("test"), postgres.WithPassword("test"), testcontainers.WithWaitStrategy(wait.ForLog("ready to accept connections").WithOccurrence(2)), ) require.NoError(t, err) t.Cleanup(func() { _ = pg.Terminate(ctx) })
dsn, _ := pg.ConnectionString(ctx, "sslmode=disable") db, err := gorm.Open(gormpg.Open(dsn)) require.NoError(t, err) runMigrations(t, db)
repo := postgres.NewUserRepository(db) user := &domain.User{ID: uuid.NewString(), Email: "i@t.com", TenantID: "t1"} require.NoError(t, repo.Create(ctx, user))
got, err := repo.GetByID(ctx, user.ID) require.NoError(t, err) assert.Equal(t, user.Email, got.Email)}Run via build tag: go test -tags=integration ./.... CI runs unit (-short) and integration in separate matrix jobs.
Concurrency tests
go test -race ./... is non-negotiable on CI. For services using conc, also test with GOMAXPROCS=1 to flush serialized-execution bugs.
Benchmarks
func BenchmarkUserService_BatchGet(b *testing.B) { svc := setupBench(b) ids := generateIDs(100) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = svc.BatchGet(context.Background(), ids) }}Run on every release: task test:bench. Compare with benchstat to catch regressions.
Coverage targets
- Domain & service: ≥ 80% line coverage
- Repository: integration test exercises the real schema; line coverage less important
- Handler: at least one happy-path + one error-path table case per route
task test:coverage produces coverage.html. CI fails the build if coverage drops > 2pp from main.