cristiano-pacheco

go-integration-tests

Generate comprehensive Go integration tests using testify suite patterns with real database and infrastructure dependencies. Use when creating or updating integration test files, testing use cases against real databases, verifying end-to-end flows, or when asked to add integration test coverage for Go code.

cristiano-pacheco 0 Updated 3mo ago
GitHub

Install

npx skillscat add cristiano-pacheco/ai-rules/go-integration-tests

Install via the SkillsCat registry.

SKILL.md

Go Integration Tests

Generate comprehensive Go integration tests using testify suite patterns with real database and infrastructure dependencies.

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.gotest/integration/modules/identity/usecase/user/user_register_usecase_test.go
  2. Dependencies: Identify which real dependencies (database, redis) vs mocked dependencies (email, external APIs)
  3. Test Cases: Define scenarios covering happy paths, edge cases, and error conditions
  4. Naming: Number each test case clearly (e.g., TestExecute_ValidInput_ReturnsUser, TestExecute_DuplicateEmail_ReturnsError)

Show the code without explanations during planning.

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), and db fields
  • Implement SetupSuite to start containers and run migrations (runs once)
  • Implement TearDownSuite to stop containers (runs once)
  • Implement SetupTest to truncate tables and initialize sut (runs before each test)
  • Use //go:build integration build tag at the top of the file
  • Always use _test suffix for package name
  • Use suite methods for assertions (e.g., suite.Equal(v, 10))
  • Use suite.Require() for error assertions (e.g., suite.Require().ErrorIs, suite.Require().Error)
  • Never use .AssertExpectations(s.T())

Example:

//go:build integration

package user_test

import (
	"context"
	"testing"

	"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/repository"
	"github.com/cristiano-pacheco/pingo/internal/modules/identity/usecase/user"
	"github.com/cristiano-pacheco/pingo/internal/shared/config"
	"github.com/cristiano-pacheco/pingo/internal/shared/database"
	"github.com/cristiano-pacheco/pingo/test/mocks"
)

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
	cfg            config.Config
}

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()
	}
}

func (s *UserRegisterUseCaseTestSuite) SetupTest() {
	s.kit.TruncateTables(s.T())

	s.emailSender = mocks.NewMockEmailSender(s.T())
	s.cfg = s.createTestConfig()
	s.sut = s.createTestUseCase()
}

func (s *UserRegisterUseCaseTestSuite) createTestConfig() config.Config {
	return config.Config{
		App: config.AppConfig{
			BaseURL: "http://test.example.com",
		},
	}
}

func (s *UserRegisterUseCaseTestSuite) createTestUseCase() *user.UserRegisterUseCase {
	log := new(mocks.MockLogger)

	v, err := validator.New()
	s.Require().NoError(err)

	userRepo := repository.NewUserRepository(s.db)

	return user.NewUserRegisterUseCase(
		userRepo,
		s.emailSender,
		v,
		s.cfg,
		log,
	)
}

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
	s.Require().NoError(err)
	s.NotZero(output.ID)
	s.Equal(input.Email, output.Email)

	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)
}

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
	_, err := s.sut.Execute(ctx, input)
	s.Require().NoError(err)

	// Act - Second registration with same email
	_, err = s.sut.Execute(ctx, input)

	// Assert
	s.Require().Error(err)
	s.ErrorIs(err, errs.ErrDuplicateEmail)
}

Mock Rules for Integration Tests

  • Mock external services (email, SMS, external APIs) that cannot run locally
  • Use real database connections (via itestkit)
  • Use real Redis connections when testing cache (via itestkit)
  • Mock metrics and logger dependencies with .Maybe() to allow optional calls
  • Always pass mock.Anything for context parameters

Example with Mocks:

func (s *UserRegisterUseCaseTestSuite) SetupTest() {
	s.kit.TruncateTables(s.T())

	s.emailSender = mocks.NewMockEmailSender(s.T())
	s.tokenGenerator = mocks.NewMockTokenGenerator(s.T())

	// Setup optional mock expectations
	s.emailSender.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
		Return(nil).Maybe()
	s.tokenGenerator.On("GenerateToken").Return("test-token", nil).Maybe()

	s.sut = s.createTestUseCase()
}

Test Structure Requirements

(CRITICAL) Arrange-Act-Assert Pattern

Every test must follow AAA pattern with explicit comments:

// Arrange
// Act
// Assert

Code Style

  • Never use inline struct construction; always create variable first
  • Maximum 120 characters per line
  • Test names must clearly indicate what is being tested
  • Add comments for complex test setups or assertions

Test Coverage

  • Include happy path scenarios
  • Include edge cases
  • Include error handling
  • Verify database state after operations
  • Test data persistence and retrieval

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-integration

Completion

When tests are complete, respond with: Integration Tests Done, Oh Yeah!