go-integration-tests

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Integration Tests

Go集成测试

Generate comprehensive Go integration tests using testify suite patterns with real database and infrastructure dependencies.
使用testify suite模式结合真实数据库与基础设施依赖项,生成全面的Go集成测试。

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 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.
编写测试前,需明确:
  1. 测试文件位置:测试文件放在
    test/integration/
    目录下,与
    internal/
    中的源码路径一一对应
    • 示例:
      internal/modules/identity/usecase/user/user_register_usecase.go
      test/integration/modules/identity/usecase/user/user_register_usecase_test.go
  2. 依赖项区分:明确哪些是真实依赖项(数据库、Redis),哪些是模拟依赖项(邮件、外部API)
  3. 测试用例定义:覆盖正常流程、边界情况及错误场景的测试场景
  4. 命名规范:为每个测试用例清晰编号(例如:
    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), 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
//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)
}
结合itestkit,使用testify的
suite.Suite
来实现容器化基础设施的集成测试。
核心规则:
  • 创建包含
    sut
    (被测系统)、
    kit
    (ITestKit)和
    db
    字段的套件结构体
  • 实现
    SetupSuite
    方法启动容器并执行迁移(仅运行一次)
  • 实现
    TearDownSuite
    方法停止容器(仅运行一次)
  • 实现
    SetupTest
    方法清空表数据并初始化sut(每个测试前运行)
  • 在文件顶部添加
    //go:build integration
    构建标签
  • 包名必须以
    _test
    结尾
  • 使用
    suite
    方法进行断言(例如:
    suite.Equal(v, 10)
  • 使用
    suite.Require()
    进行错误断言(例如:
    suite.Require().ErrorIs
    suite.Require().Error
  • 禁止使用
    .AssertExpectations(s.T())
示例:
go
//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规则

  • 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:
go
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()
}
  • 对无法在本地运行的外部服务(邮件、短信、外部API)进行Mock
  • 通过itestkit使用真实数据库连接
  • 测试缓存时通过itestkit使用真实Redis连接
  • 对指标和日志依赖项使用
    .Maybe()
    进行Mock,允许可选调用
  • 上下文参数始终传递
    mock.Anything
Mock示例:
go
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

(关键)Arrange-Act-Assert(AAA)模式

Every test must follow AAA pattern with explicit comments:
go
// Arrange
// Act
// Assert
每个测试必须遵循AAA模式,并添加明确的注释:
go
// 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
  • 禁止使用内联结构体构造,必须先创建变量
  • 每行最多120个字符
  • 测试名称必须清晰表明测试内容
  • 复杂的测试设置或断言需添加注释

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 FileIntegration 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
集成测试文件在
test/integration/
目录下镜像源码结构:
源码文件集成测试文件
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

运行集成测试

bash
undefined
bash
undefined

Run all integration tests

Run all integration tests

make test-integration
undefined
make test-integration
undefined

Completion

完成标识

When tests are complete, respond with: Integration Tests Done, Oh Yeah!
测试完成后,回复:Integration Tests Done, Oh Yeah!