Golang测试中如何实现数据库事务操作

Golang测试中如何实现数据库事务操作 你好,

我一直在编写一些集成测试,并在我的应用中使用gorm作为ORM。我试图设置测试,使得测试中的所有数据库操作都在一个事务中运行,并且每次都会回滚,这样我每次运行测试时都可以从一个干净的状态开始。以下是我目前的代码:

// 在辅助包中的某个位置
func SetupTestDB() (*gorm.DB, func()) {
    dbURI := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True", ....)

    db, err := gorm.Open("mysql", dbURI)
    if err != nil {
        panic(err)
    }

    tx := db.Begin()

    cleanup := func() {
        tx.Rollback()
        db.Close()
    }

    return tx, cleanup
}


// 在测试本身中
func TestDoSomething(t *testing.T) {
    db, cleanup := tests.SetupTestDB()
    defer cleanup()

    user := factory.CreateUser(db)
    service := someService.New(db)
    // ....

    service.DoSomethingElse(user, "param 1")
    //
}

在大多数情况下,它是有效的。

但是,时不时地,它似乎会随机失败,错误信息表明应用无法从数据库中找到相应的记录,即使该记录应该已经被测试中的工厂函数调用创建了。

从错误看似随机的性质来看,我猜这可能与go并行运行测试的方式有关?但我不确定该从哪里开始调查这个问题,因为所有操作都应该在它们自己独立的事务中运行?


更多关于Golang测试中如何实现数据库事务操作的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

没关系,我找到问题了。原来是件小事,跟事务或竞态条件无关。

其中一个工厂方法使用了伪造库来生成短链接,有时它会生成一个比数据库字段更长的短链接,而这个长链接会被自动截断。

更多关于Golang测试中如何实现数据库事务操作的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang测试中实现数据库事务操作时,你遇到的问题很可能是由于测试并行执行导致的事务隔离问题。以下是一个更可靠的实现方案:

// 在辅助包中
package tests

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "sync"
)

var (
    db     *gorm.DB
    dbOnce sync.Once
)

func SetupTestDB() (*gorm.DB, func()) {
    dbOnce.Do(func() {
        dsn := "user:pass@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
        var err error
        db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
            SkipDefaultTransaction: true,
        })
        if err != nil {
            panic(err)
        }
    })

    // 每个测试获取自己的事务
    tx := db.Begin()
    
    cleanup := func() {
        tx.Rollback()
    }
    
    return tx, cleanup
}

// 或者使用更明确的事务管理
func WithTransaction(t *testing.T, testFunc func(*gorm.DB)) {
    db, cleanup := SetupTestDB()
    defer cleanup()
    
    testFunc(db)
}
// 在测试中使用
package service_test

import (
    "testing"
    "github.com/yourproject/tests"
)

func TestDoSomething(t *testing.T) {
    tests.WithTransaction(t, func(db *gorm.DB) {
        user := factory.CreateUser(db)
        service := someService.New(db)
        
        err := service.DoSomethingElse(user, "param 1")
        if err != nil {
            t.Errorf("DoSomethingElse failed: %v", err)
        }
        
        // 验证结果
        var count int64
        db.Model(&User{}).Where("id = ?", user.ID).Count(&count)
        if count != 1 {
            t.Errorf("Expected 1 user, got %d", count)
        }
    })
}

// 或者直接使用
func TestDoSomethingDirect(t *testing.T) {
    db, cleanup := tests.SetupTestDB()
    defer cleanup()
    
    // 确保在事务中执行所有操作
    err := db.Transaction(func(tx *gorm.DB) error {
        user := factory.CreateUser(tx)
        service := someService.New(tx)
        return service.DoSomethingElse(user, "param 1")
    })
    
    if err != nil {
        t.Errorf("Transaction failed: %v", err)
    }
}
// 工厂函数示例
package factory

import "gorm.io/gorm"

func CreateUser(db *gorm.DB) *User {
    user := &User{
        Name:  "test_user",
        Email: "test@example.com",
    }
    
    if err := db.Create(user).Error; err != nil {
        panic(err)
    }
    
    return user
}

关键点:

  1. 使用 sync.Once 确保数据库连接只初始化一次
  2. 每个测试获取独立的事务实例
  3. 确保工厂函数和业务逻辑都使用相同的事务实例
  4. 使用 defer cleanup() 确保事务总是回滚
  5. 考虑使用 t.Parallel() 时的事务隔离级别

如果问题仍然存在,可以在测试开始时设置事务隔离级别:

func SetupTestDB() (*gorm.DB, func()) {
    // ... 初始化代码
    
    tx := db.Begin()
    // 设置读已提交隔离级别
    tx.Exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
    
    cleanup := func() {
        tx.Rollback()
    }
    
    return tx, cleanup
}
回到顶部