Generate Go integration tests with real database/infrastructure via itestkit containers. Use when testing use cases against real databases, verifying end-to-end flows, or adding integration test coverage.
Generate comprehensive Go integration tests using testify suite patterns with real database and infrastructure dependencies.
Before writing tests, identify:
test/integration/ mirroring the source path from internal/
internal/modules/identity/usecase/user/user_register_usecase.go → test/integration/modules/identity/usecase/user/user_register_usecase_test.goTestExecute_ValidInput_ReturnsUser, TestExecute_DuplicateEmail_ReturnsErrorUse suite.Suite from testify with itestkit for containerized infrastructure.
Key Rules:
sut (System Under Test), kit (ITestKit), and db fieldsSetupSuite to start containers and run migrations (runs once per suite)TearDownSuite to stop containers (runs once per suite)SetupTest to truncate tables, create fresh mock objects, and reinitialize sut (runs before each test)//go:build integration build tag at the top of the file_test suffix for package namesuite methods for assertions (e.g., s.Equal(...), s.NotZero(...))s.Require() for fatal assertions (e.g., s.Require().NoError(err), s.Require().ErrorIs(...)).AssertExpectations(s.T()) — testify does this automaticallyFull Example:
//go:build integration
package user_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"github.com/cristiano-pacheco/bricks/pkg/itestkit"
"github.com/cristiano-pacheco/bricks/pkg/validator"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/errs"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/model"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/repository"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/usecase/user"
identity_validator "github.com/cristiano-pacheco/pingo/internal/modules/identity/validator"
"github.com/cristiano-pacheco/pingo/internal/shared/config"
"github.com/cristiano-pacheco/pingo/internal/shared/database"
"github.com/cristiano-pacheco/pingo/test/mocks"
)
// emailRecord captures emails sent during tests for assertion purposes.
type emailRecord struct {
to string
subject string
body string
}
func TestMain(m *testing.M) {
itestkit.TestMain(m)
}
type UserRegisterUseCaseTestSuite struct {
suite.Suite
kit *itestkit.ITestKit
db *database.PingoDB
sut *user.UserRegisterUseCase
emailSender *mocks.MockEmailSender
tokenGenerator *mocks.MockTokenGenerator
cfg config.Config
sentEmails []emailRecord
}
func TestUserRegisterUseCaseSuite(t *testing.T) {
suite.Run(t, new(UserRegisterUseCaseTestSuite))
}
func (s *UserRegisterUseCaseTestSuite) SetupSuite() {
s.kit = itestkit.New(itestkit.Config{
PostgresImage: "postgres:16-alpine",
RedisImage: "redis:7-alpine",
MigrationsPath: "file://migrations",
Database: "pingo_test",
User: "pingo_test",
Password: "pingo_test",
})
err := s.kit.StartPostgres()
s.Require().NoError(err)
err = s.kit.RunMigrations()
s.Require().NoError(err)
s.db = &database.PingoDB{DB: s.kit.DB()}
}
func (s *UserRegisterUseCaseTestSuite) TearDownSuite() {
if s.kit != nil {
s.kit.StopPostgres()
}
}
// SetupTest runs before every test. Create fresh mock objects here and reset
// any captured side-effect state. Then call createTestUseCase to wire everything up.
func (s *UserRegisterUseCaseTestSuite) SetupTest() {
s.kit.TruncateTables(s.T())
s.sentEmails = nil
s.emailSender = mocks.NewMockEmailSender(s.T())
s.tokenGenerator = mocks.NewMockTokenGenerator(s.T())
s.cfg = s.createTestConfig(true)
s.sut = s.createTestUseCase()
}
// createTestConfig accepts feature flags so individual tests can reconfigure the SUT.
func (s *UserRegisterUseCaseTestSuite) createTestConfig(registrationEnabled bool) config.Config {
return config.Config{
App: config.AppConfig{
BaseURL: "http://test.example.com",
Identity: config.IdentityConfig{
Registration: config.RegistrationConfig{
Enabled: registrationEnabled,
},
},
},
}
}
// createTestUseCase wires all dependencies and sets up mock expectations.
// Infrastructure mocks (metrics, logger) use .Maybe() so they satisfy calls
// without requiring them. Domain mocks (emailSender, tokenGenerator) use .Run()
// callbacks to capture side effects for later assertion.
func (s *UserRegisterUseCaseTestSuite) createTestUseCase() *user.UserRegisterUseCase {
log := new(mocks.MockLogger)
v, err := validator.New()
s.Require().NoError(err)
pwValidator := identity_validator.NewPasswordValidator()
pwHasher := service.NewPasswordHasherService(s.cfg)
useCaseMetrics := new(mocks.MockUseCaseMetrics)
useCaseMetrics.On("ObserveDuration", mock.Anything, mock.Anything).Return().Maybe()
useCaseMetrics.On("IncrementCount", mock.Anything).Return().Maybe()
useCaseMetrics.On("IncSuccess", mock.Anything).Return().Maybe()
useCaseMetrics.On("IncError", mock.Anything).Return().Maybe()
s.tokenGenerator.On("GenerateToken").Return("test-token-12345", nil).Maybe()
s.tokenGenerator.On("HashToken", mock.Anything).Return(func(token string) []byte {
hash := make([]byte, len(token))
copy(hash, token)
return hash
}).Maybe()
// Capture emails for later assertion in test methods.
s.emailSender.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
s.sentEmails = append(s.sentEmails, emailRecord{
to: args.String(1),
subject: args.String(2),
body: args.String(3),
})
}).Return(nil).Maybe()
userRepo := repository.NewUserRepository(s.db)
tokenRepo := repository.NewIdentityTokenRepository(s.db)
return user.NewUserRegisterUseCase(
userRepo,
tokenRepo,
pwHasher,
pwValidator,
s.tokenGenerator,
s.emailSender,
v,
s.cfg,
log,
useCaseMetrics,
)
}
// findSentEmail is a suite helper for looking up captured emails by recipient.
func (s *UserRegisterUseCaseTestSuite) findSentEmail(email string) *emailRecord {
for i := range s.sentEmails {
if s.sentEmails[i].to == email {
return &s.sentEmails[i]
}
}
return nil
}
func (s *UserRegisterUseCaseTestSuite) TestExecute_ValidInput_ReturnsUser() {
// Arrange
ctx := context.Background()
input := user.UserRegisterInput{
Email: "[email protected]",
Password: "Password123!",
FirstName: "John",
LastName: "Doe",
}
// Act
output, err := s.sut.Execute(ctx, input)
// Assert — output fields
s.Require().NoError(err)
s.NotZero(output.ID)
s.Equal(input.Email, output.Email)
s.Equal(input.FirstName, output.FirstName)
s.Equal(input.LastName, output.LastName)
s.Equal("pending_verification", output.Status)
// Assert — DB state
var savedUser model.UserModel
err = s.db.DB.Where("id = ?", output.ID).First(&savedUser).Error
s.Require().NoError(err)
s.Equal(input.Email, savedUser.Email)
s.Equal(input.FirstName, savedUser.FirstName)
s.Equal(input.LastName, savedUser.LastName)
s.NotEmpty(savedUser.PasswordHash)
s.NotZero(savedUser.CreatedAt)
s.NotZero(savedUser.UpdatedAt)
// Assert — token persisted
var savedToken model.IdentityTokenModel
err = s.db.DB.Where("user_id = ? AND token_type = ?", output.ID, "email_verification").
First(&savedToken).Error
s.Require().NoError(err)
s.True(savedToken.ExpiresAt.After(time.Now()))
// Assert — email side effect
email := s.findSentEmail(input.Email)
s.Require().NotNil(email)
s.Equal("Verify your email", email.subject)
s.Contains(email.body, "test-token-12345")
}
func (s *UserRegisterUseCaseTestSuite) TestExecute_DuplicateEmail_ReturnsError() {
// Arrange
ctx := context.Background()
input := user.UserRegisterInput{
Email: "[email protected]",
Password: "Password123!",
FirstName: "John",
LastName: "Doe",
}
// Act - First registration succeeds
output1, err := s.sut.Execute(ctx, input)
s.Require().NoError(err)
s.NotZero(output1.ID)
s.sentEmails = nil
// Act - Second registration with same email
_, err = s.sut.Execute(ctx, user.UserRegisterInput{
Email: "[email protected]",
Password: "Different456!",
FirstName: "Jane",
LastName: "Smith",
})
// Assert
s.Require().Error(err)
s.ErrorIs(err, errs.ErrDuplicateEmail)
var count int64
s.db.DB.Model(&model.UserModel{}).Where("email = ?", input.Email).Count(&count)
s.Equal(int64(1), count)
s.Nil(s.findSentEmail(input.Email))
}
func (s *UserRegisterUseCaseTestSuite) TestExecute_RegistrationDisabled_ReturnsError() {
// Arrange - recreate sut with registration disabled
s.cfg = s.createTestConfig(false)
s.sut = s.createTestUseCase()
ctx := context.Background()
input := user.UserRegisterInput{
Email: "[email protected]",
Password: "Password123!",
FirstName: "John",
LastName: "Doe",
}
// Act
_, err := s.sut.Execute(ctx, input)
// Assert
s.Require().Error(err)
s.ErrorIs(err, errs.ErrRegistrationDisabled)
var count int64
s.db.DB.Model(&model.UserModel{}).Where("email = ?", input.Email).Count(&count)
s.Equal(int64(0), count)
s.Nil(s.findSentEmail(input.Email))
}
What to mock vs. what to use for real:
Mock setup placement:
SetupTest (fresh instance per test).On(...) expectations inside createTestUseCase.Maybe() on infrastructure mocks (logger, metrics) — they may or may not be called.Run() callbacks on domain mocks (emailSender, tokenGenerator) to capture side effects for later assertionmock.Anything for context parametersSide-effect capture pattern — when you need to assert on things like emails sent:
// 1. Define a record type at the top of the file
type emailRecord struct {
to string
subject string
body string
}
// 2. Add a slice field to the suite and a helper method
type MyTestSuite struct {
suite.Suite
sentEmails []emailRecord
// ...
}
func (s *MyTestSuite) findSentEmail(to string) *emailRecord {
for i := range s.sentEmails {
if s.sentEmails[i].to == to {
return &s.sentEmails[i]
}
}
return nil
}
// 3. Reset and capture in SetupTest / createTestUseCase
func (s *MyTestSuite) SetupTest() {
s.sentEmails = nil
// ...
}
// In createTestUseCase:
s.emailSender.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
s.sentEmails = append(s.sentEmails, emailRecord{
to: args.String(1),
subject: args.String(2),
body: args.String(3),
})
}).Return(nil).Maybe()
Use s.Run() when testing the same behavior with multiple inputs:
func (s *UserRegisterUseCaseTestSuite) TestExecute_InvalidPassword_ReturnsError() {
// Arrange
ctx := context.Background()
testCases := []struct {
name string
email string
password string
}{
{"too short", "[email protected]", "Short1!"},
{"no uppercase", "[email protected]", "lowercase123!"},
{"no digit", "[email protected]", "NoDigitsHere!"},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
input := user.UserRegisterInput{
Email: tc.email,
Password: tc.password,
FirstName: "John",
LastName: "Doe",
}
// Act
_, err := s.sut.Execute(ctx, input)
// Assert
s.Require().Error(err)
s.ErrorIs(err, errs.ErrPasswordPolicyViolation)
var count int64
s.db.DB.Model(&model.UserModel{}).Where("email = ?", tc.email).Count(&count)
s.Equal(int64(0), count)
})
}
}
Every test must follow AAA with explicit comments:
// Arrange
// Act
// Assert
For multi-step tests (e.g., set up data then test), label each step clearly:
// Act - First registration succeeds
// Assert - First registration succeeds
// Act - Second registration with same email
// Assert - Second registration fails
Always verify more than just the return value. Assert:
Integration tests mirror the source structure under test/integration/:
| Source File | Integration Test File |
|---|---|
internal/modules/identity/usecase/user/user_register_usecase.go | test/integration/modules/identity/usecase/user/user_register_usecase_test.go |
internal/modules/monitor/usecase/metric_usecase.go | test/integration/modules/monitor/usecase/metric_usecase_test.go |
# Run all integration tests
make test-integration
When tests are complete, respond with: Integration Tests Done, Oh Yeah!