
Go Integration Tests
- 20 installs
- Updated July 22, 2026
- cristiano-pacheco/ai-tools
Generates Go integration tests using testify suites and itestkit containers to exercise use cases against real databases and infrastructure.
About
Scaffolds integration tests that run against real containerized databases and Redis via testify suites and itestkit, with build tags and DB-state assertions. A developer uses it when verifying end-to-end flows against real infrastructure.
- Uses testify suite with itestkit containers and integration build tag
- Mirrors source paths under test/integration
Go Integration Tests by the numbers
- 20 all-time installs (skills.sh)
- Ranked #1,436 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cristiano-pacheco/ai-tools --skill go-integration-testsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20 |
|---|---|
| Last updated | July 22, 2026 |
| Repository | cristiano-pacheco/ai-tools ↗ |
What it does
Generates Go integration tests using testify suites and itestkit containers to exercise use cases against real databases and infrastructure.
Files
Go Integration Tests
Generate comprehensive Go integration tests using testify suite patterns with real database and infrastructure dependencies.
When to Use
- Test use cases against real databases via containers
- Verify end-to-end flows with real infrastructure
- Assert DB state, side effects, and error conditions
Planning Phase
Before writing tests, identify:
1. Test Location: Tests go in test/integration/ mirroring the source path from internal/
- Example:
internal/modules/identity/usecase/user/user_register_usecase.go→test/integration/modules/identity/usecase/user/user_register_usecase_test.go
2. Dependencies: Identify real dependencies (database, redis) vs mocked ones (email, external APIs, metrics) 3. Test Cases: Define scenarios covering happy paths, edge cases, error conditions, and DB state verification 4. Naming: Use descriptive names: TestExecute_ValidInput_ReturnsUser, TestExecute_DuplicateEmail_ReturnsError
Implementation Patterns
Pattern: Integration Test Suite
Use suite.Suite from testify with itestkit for containerized infrastructure.
Key Rules:
- Create suite struct with
sut(System Under Test),kit(ITestKit), anddbfields - Implement
SetupSuiteto start containers and run migrations (runs once per suite) - Implement
TearDownSuiteto stop containers (runs once per suite) - Implement
SetupTestto truncate tables, create fresh mock objects, and reinitialize sut (runs before each test) - Use
//go:build integrationbuild tag at the top of the file - Always use
_testsuffix for package name - Use
suitemethods for assertions (e.g.,s.Equal(...),s.NotZero(...)) - Use
s.Require()for fatal assertions (e.g.,s.Require().NoError(err),s.Require().ErrorIs(...)) - Never use
.AssertExpectations(s.T())— testify does this automatically
Full 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: "test@example.com",
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: "test@example.com",
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: "test@example.com",
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: "test@example.com",
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))
}Mock Rules for Integration Tests
What to mock vs. what to use for real:
- Real: database (via itestkit), Redis (via itestkit), any local service
- Mock: email/SMS senders, external HTTP APIs, token generators, metrics, logger
Mock setup placement:
- Create mock objects in
SetupTest(fresh instance per test) - Set
.On(...)expectations insidecreateTestUseCase - Use
.Maybe()on infrastructure mocks (logger, metrics) — they may or may not be called - Use
.Run()callbacks on domain mocks (emailSender, tokenGenerator) to capture side effects for later assertion - Always pass
mock.Anythingfor context parameters
Side-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()Table-Driven Subtests
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", "tooshort@example.com", "Short1!"},
{"no uppercase", "nouppercase@example.com", "lowercase123!"},
{"no digit", "nodigit@example.com", "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)
})
}
}Test Structure Requirements
(CRITICAL) Arrange-Act-Assert Pattern
Every test must follow AAA with explicit comments:
// Arrange
// Act
// AssertFor 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 failsAssertion depth
Always verify more than just the return value. Assert: 1. Output fields — all fields returned by the use case 2. DB state — query the database and verify the persisted record 3. Side effects — emails sent, tokens created, counts correct 4. Negative state — on error paths, verify nothing was persisted and no emails sent
Code Style
- No standalone functions: When a file contains a struct with methods, do not add standalone functions. Use private methods on the struct instead.
- Maximum 120 characters per line
- Test names clearly state what is tested and what is expected
- Use inline struct slices for table-driven test cases (standard Go pattern)
- Add comments before complex assertions to explain what is being verified
Test File Location
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 |
Running Integration Tests
# Run all integration tests
make test-integrationCompletion
When tests are complete, respond with: Integration Tests Done, Oh Yeah!