单元测试隔离了数据库,但有些问题只有在真实数据库环境下才能发现。集成测试就是验证代码和数据库的协作是否正常。
单元测试用 Mock 或 SQLite,可能漏掉这些问题:
集成测试用真实的数据库环境,能发现这些隐藏问题。
集成测试需要一个独立的测试数据库:
func setupIntegrationDB(t *testing.T) *gorm.DB {
dsn := "test_user:test_pass@tcp(localhost:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Skipf("skip integration test: %v", err)
}
// 清理并重建表
db.Migrator().DropTable(&User{}, &Order{})
db.AutoMigrate(&User{}, &Order{})
return db
}
用 t.Skip 而不是 t.Fatal,这样没有测试环境时不会报错。
用 Docker 启动测试数据库更可靠:
func setupTestContainer(t *testing.T) (*gorm.DB, func()) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "mysql:8.0",
ExposedPorts: []string{"3306/tcp"},
Env: map[string]string{
"MYSQL_ROOT_PASSWORD": "test",
"MYSQL_DATABASE": "test_db",
},
WaitingFor: wait.ForLog("port: 3306 MySQL Community Server"),
}
mysqlContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Skipf("skip: %v", err)
return nil, func() {}
}
host, _ := mysqlContainer.Host(ctx)
port, _ := mysqlContainer.MappedPort(ctx, "3306")
dsn := fmt.Sprintf("root:test@tcp(%s:%s)/test_db?charset=utf8mb4&parseTime=True&loc=Local", host, port.Port())
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
cleanup := func() {
mysqlContainer.Terminate(ctx)
}
return db, cleanup
}
func TestIntegration(t *testing.T) {
db, cleanup := setupTestContainer(t)
defer cleanup()
db.AutoMigrate(&User{})
// 测试代码...
}
Testcontainers 的好处:
创建测试数据的工厂函数:
type UserFactory struct {
db *gorm.DB
}
func (f *UserFactory) Create(overrides ...func(*User)) *User {
user := &User{
Name: "测试用户",
Email: fmt.Sprintf("test_%d@example.com", time.Now().UnixNano()),
Status: "active",
}
for _, override := range overrides {
override(user)
}
if err := f.db.Create(user).Error; err != nil {
panic(err)
}
return user
}
// 使用
factory := &UserFactory{db: db}
user := factory.Create(func(u *User) {
u.Name = "特殊用户"
u.Status = "inactive"
})
事务相关的测试:
func TestTransactionRollback(t *testing.T) {
db := setupIntegrationDB(t)
// 初始状态
db.Create(&User{Name: "张三", Balance: 100})
// 模拟转账失败
err := db.Transaction(func(tx *gorm.DB) error {
// 扣款
if err := tx.Model(&User{}).Where("name = ?", "张三").
Update("balance", gorm.Expr("balance - ?", 100)).Error; err != nil {
return err
}
// 模拟失败
return errors.New("转账失败")
})
assert.Error(t, err)
// 验证回滚
var user User
db.Where("name = ?", "张三").First(&user)
assert.Equal(t, 100, user.Balance) // 余额不变
}
测试并发场景:
func TestConcurrentUpdate(t *testing.T) {
db := setupIntegrationDB(t)
db.Create(&User{Name: "张三", Balance: 100})
var wg sync.WaitGroup
errors := make([]error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
err := db.Transaction(func(tx *gorm.DB) error {
var user User
if err := tx.Where("name = ?", "张三").First(&user).Error; err != nil {
return err
}
time.Sleep(10 * time.Millisecond) // 模拟处理时间
return tx.Model(&user).Update("balance", user.Balance+10).Error
})
errors[idx] = err
}(i)
}
wg.Wait()
// 检查最终余额
var user User
db.Where("name = ?", "张三").First(&user)
// 可能不是预期的 200,需要加锁
}
这个测试暴露了并发更新的问题,需要用乐观锁或悲观锁解决。
验证索引是否生效:
func TestIndexUsage(t *testing.T) {
db := setupIntegrationDB(t)
// 创建大量测试数据
for i := 0; i < 10000; i++ {
db.Create(&User{
Name: fmt.Sprintf("user_%d", i),
Status: "active",
})
}
// 用 EXPLAIN 检查
var result []map[string]interface{}
db.Raw("EXPLAIN SELECT * FROM users WHERE status = ?", "active").Scan(&result)
// 检查是否用了索引
assert.Contains(t, result[0]["type"], "ref")
assert.Contains(t, result[0]["key"], "idx_status")
}
在 CI 中运行集成测试:
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
integration:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test_db
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run integration tests
run: go test -tags=integration ./...
env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
集成测试的要点:
集成测试比单元测试慢,但能发现更多问题。建议核心业务逻辑都要有集成测试覆盖。