测试与 Mock 实践
单元测试需隔离数据库依赖,本文介绍使用 sqlmock 模拟数据库进行 GORM 测试的实践。
环境准备
安装依赖
Bash
go get github.com/DATA-DOG/go-sqlmock
初始化 Mock
Go
import (
"testing"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"github.com/DATA-DOG/go-sqlmock"
)
func setupMock(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
// 将 mock 连接传入 GORM
dialector := mysql.New(mysql.Config{
Conn: mockDB,
SkipInitializeWithVersion: true,
})
db, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
t.Fatalf("failed to open gorm: %v", err)
}
return db, mock
}
CRUD 测试
查询测试
Go
func TestFindUser(t *testing.T) {
db, mock := setupMock(t)
// 预期 SQL
expectedSQL := "SELECT * FROM `users` WHERE `users`.`id` = ?"
// 设置期望与返回
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "test", "test@example.com")
mock.ExpectQuery(regexp.QuoteMeta(expectedSQL)).
WithArgs(1).
WillReturnRows(rows)
// 执行查询
var user User
err := db.First(&user, 1).Error
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// 验证
if user.Name != "test" {
t.Errorf("expected name=test, got %s", user.Name)
}
// 确认所有期望都被满足
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %v", err)
}
}
插入测试
Go
func TestCreateUser(t *testing.T) {
db, mock := setupMock(t)
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(
"INSERT INTO `users` (`name`,`email`) VALUES (?,?)")).
WithArgs("test", "test@example.com").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
user := User{Name: "test", Email: "test@example.com"}
err := db.Create(&user).Error
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %v", err)
}
}
更新测试
Go
func TestUpdateUser(t *testing.T) {
db, mock := setupMock(t)
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(
"UPDATE `users` SET `name`=? WHERE `id` = ?")).
WithArgs("updated", 1).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
err := db.Model(&User{}).Where("id = ?", 1).Update("name", "updated").Error
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
删除测试
Go
func TestDeleteUser(t *testing.T) {
db, mock := setupMock(t)
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(
"DELETE FROM `users` WHERE `users`.`id` = ?")).
WithArgs(1).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
err := db.Delete(&User{}, 1).Error
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
事务测试
事务提交
Go
func TestTransactionCommit(t *testing.T) {
db, mock := setupMock(t)
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
err := db.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{Name: "user1"})
tx.Model(&User{}).Where("id = ?", 1).Update("name", "updated")
return nil
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
事务回滚
Go
func TestTransactionRollback(t *testing.T) {
db, mock := setupMock(t)
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectRollback()
err := db.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{Name: "user1"})
return errors.New("business error") // 触发回滚
})
if err == nil {
t.Fatal("expected error, got nil")
}
}
注意事项
regexp.QuoteMeta用于转义 GORM 生成的 SQL 中的反引号等字符,确保正则匹配正确。
复杂查询的 SQL 可能因驱动不同而变化,测试时应使用与生产相同的驱动。
sqlmock不验证 SQL 语义正确性,仅匹配字符串模式。
预期顺序必须与实际执行顺序一致,否则测试失败。
要点总结
sqlmock通过模拟*sql.DB连接实现 GORM 测试- 每个 CRUD 操作需设置对应的 Expect 与 Return
- 事务测试需匹配 Begin/Commit/Rollback 序列
- 使用
ExpectationsWereMet()验证所有期望被满足 - Mock 测试不依赖真实数据库,执行速度快
存放路径:D:\git2\jwdev\articles\GORM\专家\生态与工具链\测试与 Mock 实践.md
📝 发现内容有误?点击此处直接编辑