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套件模式,生成使用真实数据库和基础设施依赖的完整Go集成测试。

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
编写测试前,先明确以下内容:
  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)和需要mock的依赖(邮件、外部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 per suite)
  • Implement
    TearDownSuite
    to stop containers (runs once per suite)
  • Implement
    SetupTest
    to truncate tables, create fresh mock objects, and reinitialize 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.,
    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
//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))
}
使用testify的
suite.Suite
结合itestkit实现容器化基础设施。
核心规则:
  • 定义测试套件结构体,包含
    sut
    (被测系统)、
    kit
    (ITestKit)和
    db
    字段
  • 实现
    SetupSuite
    方法启动容器并执行数据库迁移(每个套件仅执行一次)
  • 实现
    TearDownSuite
    方法停止容器(每个套件仅执行一次)
  • 实现
    SetupTest
    方法清空表数据、创建新的mock对象、重新初始化sut(每个测试用例执行前都会运行)
  • 文件顶部添加
    //go:build integration
    构建标签
  • 包名必须使用
    _test
    后缀
  • 使用
    suite
    提供的断言方法(例如
    s.Equal(...)
    s.NotZero(...)
  • 致命断言使用
    s.Require()
    (例如
    s.Require().NoError(err)
    s.Require().ErrorIs(...)
  • 无需手动调用
    .AssertExpectations(s.T())
    —— testify会自动执行该操作
完整示例:
go
//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 捕获测试过程中发送的邮件,用于后续断言
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 每个测试用例执行前运行。在此处创建全新的mock对象,重置所有捕获的副作用状态,然后调用createTestUseCase组装所有依赖
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 接收功能开关参数,允许单个测试用例重新配置被测系统
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 组装所有依赖并设置mock预期。基础设施mock(监控指标、日志)使用.Maybe(),允许被调用也允许不被调用。领域mock(emailSender、tokenGenerator)使用.Run()回调捕获副作用,用于后续断言
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()

	// 捕获邮件,用于后续测试方法中断言
	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 套件辅助方法,通过收件人查找捕获的邮件
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 — 输出字段校验
	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 — 数据库状态校验
	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持久化校验
	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 := 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 - 第一次注册成功
	output1, err := s.sut.Execute(ctx, input)
	s.Require().NoError(err)
	s.NotZero(output1.ID)
	s.sentEmails = nil

	// Act - 第二次使用相同邮箱注册
	_, 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 - 重新创建sut,关闭注册功能
	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

集成测试Mock规则

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 inside
    createTestUseCase
  • 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.Anything
    for context parameters
Side-effect capture pattern — when you need to assert on things like emails sent:
go
// 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()
哪些要mock,哪些用真实实例:
  • 真实实例:数据库(通过itestkit)、Redis(通过itestkit)、所有本地服务
  • Mock:邮件/短信发送服务、外部HTTP API、token生成器、监控指标、日志
Mock设置位置:
  • SetupTest
    中创建mock对象(每个测试用例使用全新实例)
  • createTestUseCase
    中设置
    .On(...)
    预期
  • 基础设施mock(日志、监控指标)使用
    .Maybe()
    —— 允许被调用也可以不被调用
  • 领域mock(emailSender、tokenGenerator)使用
    .Run()
    回调捕获副作用,用于后续断言
  • context参数统一传入
    mock.Anything
副作用捕获模式 —— 当你需要断言发送的邮件这类内容时:
go
// 1. 在文件顶部定义记录结构体
type emailRecord struct {
	to      string
	subject string
	body    string
}

// 2. 给测试套件添加一个切片字段和辅助方法
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. 在SetupTest / createTestUseCase中重置并捕获数据
func (s *MyTestSuite) SetupTest() {
	s.sentEmails = nil
	// ...
}

// 在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:
go
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)
		})
	}
}
当使用多组输入测试同一种行为时,使用
s.Run()
go
func (s *UserRegisterUseCaseTestSuite) TestExecute_InvalidPassword_ReturnsError() {
	// Arrange
	ctx := context.Background()
	testCases := []struct {
		name     string
		email    string
		password string
	}{
		{"密码过短", "tooshort@example.com", "Short1!"},
		{"无大写字母", "nouppercase@example.com", "lowercase123!"},
		{"无数字", "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

(关键要求) 安排-执行-断言(AAA)模式

Every test must follow AAA with explicit comments:
go
// Arrange
// Act
// Assert
For multi-step tests (e.g., set up data then test), label each step clearly:
go
// Act - First registration succeeds
// Assert - First registration succeeds
// Act - Second registration with same email
// Assert - Second registration fails
每个测试必须遵循AAA模式,添加明确的注释:
go
// Arrange 安排
// Act 执行
// Assert 断言
对于多步骤测试(例如先设置数据再测试),给每个步骤添加清晰的标签:
go
// Act - 第一次注册成功
// Assert - 第一次注册成功校验
// Act - 第二次使用相同邮箱注册
// Assert - 第二次注册失败校验

Assertion 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
除了返回值外,必须校验更多内容,包括:
  1. 输出字段 —— 校验用例返回的所有字段
  2. 数据库状态 —— 查询数据库,校验持久化的记录是否符合预期
  3. 副作用 —— 邮件发送、token创建、计数是否正确
  4. 负向状态 —— 错误路径下,校验没有数据被持久化、没有邮件被发送

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
  • 不要独立函数:如果文件中已经有带方法的结构体,不要添加独立函数,使用结构体的私有方法替代
  • 每行最多120个字符
  • 测试名称要清晰说明测试内容和预期结果
  • 表驱动测试用例使用内联结构体切片(标准Go模式)
  • 复杂断言前添加注释,说明校验的内容

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

运行所有集成测试

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!