go-integration-tests
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo 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:
- Test Location: Tests go in mirroring the source path from
test/integration/internal/- Example: →
internal/modules/identity/usecase/user/user_register_usecase.gotest/integration/modules/identity/usecase/user/user_register_usecase_test.go
- Example:
- Dependencies: Identify real dependencies (database, redis) vs mocked ones (email, external APIs, metrics)
- Test Cases: Define scenarios covering happy paths, edge cases, error conditions, and DB state verification
- Naming: Use descriptive names: ,
TestExecute_ValidInput_ReturnsUserTestExecute_DuplicateEmail_ReturnsError
编写测试前,先明确以下内容:
- 测试文件位置:测试文件放在目录下,路径与
test/integration/下的源码路径一一对应internal/- 示例:→
internal/modules/identity/usecase/user/user_register_usecase.gotest/integration/modules/identity/usecase/user/user_register_usecase_test.go
- 示例:
- 依赖梳理:区分真实依赖(数据库、redis)和需要mock的依赖(邮件、外部API、监控指标)
- 测试用例:定义覆盖正常流程、边界场景、错误场景和数据库状态校验的测试场景
- 命名规范:使用描述性命名,例如、
TestExecute_ValidInput_ReturnsUserTestExecute_DuplicateEmail_ReturnsError
Implementation Patterns
实现模式
Pattern: Integration Test Suite
模式:集成测试套件
Use from testify with itestkit for containerized infrastructure.
suite.SuiteKey Rules:
- Create suite struct with (System Under Test),
sut(ITestKit), andkitfieldsdb - Implement to start containers and run migrations (runs once per suite)
SetupSuite - Implement to stop containers (runs once per suite)
TearDownSuite - Implement to truncate tables, create fresh mock objects, and reinitialize sut (runs before each test)
SetupTest - Use build tag at the top of the file
//go:build integration - Always use suffix for package name
_test - Use methods for assertions (e.g.,
suite,s.Equal(...))s.NotZero(...) - Use for fatal assertions (e.g.,
s.Require(),s.Require().NoError(err))s.Require().ErrorIs(...) - Never use — testify does this automatically
.AssertExpectations(s.T())
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的结合itestkit实现容器化基础设施。
suite.Suite核心规则:
- 定义测试套件结构体,包含(被测系统)、
sut(ITestKit)和kit字段db - 实现方法启动容器并执行数据库迁移(每个套件仅执行一次)
SetupSuite - 实现方法停止容器(每个套件仅执行一次)
TearDownSuite - 实现方法清空表数据、创建新的mock对象、重新初始化sut(每个测试用例执行前都会运行)
SetupTest - 文件顶部添加构建标签
//go:build integration - 包名必须使用后缀
_test - 使用提供的断言方法(例如
suite、s.Equal(...))s.NotZero(...) - 致命断言使用(例如
s.Require()、s.Require().NoError(err))s.Require().ErrorIs(...) - 无需手动调用—— testify会自动执行该操作
.AssertExpectations(s.T())
完整示例:
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 (fresh instance per test)
SetupTest - Set expectations inside
.On(...)createTestUseCase - Use on infrastructure mocks (logger, metrics) — they may or may not be called
.Maybe() - Use callbacks on domain mocks (emailSender, tokenGenerator) to capture side effects for later assertion
.Run() - Always pass for context parameters
mock.Anything
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设置位置:
- 在中创建mock对象(每个测试用例使用全新实例)
SetupTest - 在中设置
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 when testing the same behavior with multiple inputs:
s.Run()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
// AssertFor 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:
- Output fields — all fields returned by the use case
- DB state — query the database and verify the persisted record
- Side effects — emails sent, tokens created, counts correct
- Negative state — on error paths, verify nothing was persisted and no emails sent
除了返回值外,必须校验更多内容,包括:
- 输出字段 —— 校验用例返回的所有字段
- 数据库状态 —— 查询数据库,校验持久化的记录是否符合预期
- 副作用 —— 邮件发送、token创建、计数是否正确
- 负向状态 —— 错误路径下,校验没有数据被持久化、没有邮件被发送
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 File | Integration Test File |
|---|---|
| |
| |
集成测试路径与源码结构对应,放在目录下:
test/integration/| 源文件 | 集成测试文件 |
|---|---|
| |
| |
Running Integration Tests
运行集成测试
bash
undefinedbash
undefinedRun all integration tests
运行所有集成测试
make test-integration
undefinedmake test-integration
undefinedCompletion
完成标识
When tests are complete, respond with: Integration Tests Done, Oh Yeah!
测试编写完成后,回复:Integration Tests Done, Oh Yeah!