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
}
关键点:
- 使用
sync.Once确保数据库连接只初始化一次 - 每个测试获取独立的事务实例
- 确保工厂函数和业务逻辑都使用相同的事务实例
- 使用
defer cleanup()确保事务总是回滚 - 考虑使用
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
}

