Skip to content

Testing

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/go-backend-engineer/testing.md
DescriptionNot 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

Terminal window
task gen:mocks
# runs: mockery --dir=internal/repository --all --output=test/mocks --outpkg=mocks

Commit 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.