go-sqlmock
gosqlmock是一个用于模拟数据库 /sql 驱动的库,核心作用是在不依赖真实数据库实例的情况下,对数据库相关逻辑进行单元测试,避免测试过程中操作真实数据、产生脏数据或依赖数据库服务可用性。
优点:
- 解除真实数据库依赖,保证测试独立、稳定、无脏数据
- 精准控制数据库行为,覆盖常规 / 异常全量测试场景
- 兼容
database/sql标准库和主流 ORM,无侵入式集成 - 严格验证预期行为,提升测试准确性,发现隐藏问题
- 轻量级无冗余,内存级执行,测试性能优异
- 支持正则匹配,灵活适配复杂 SQL 场景
1.安装
github地址
go get github.com/DATA-DOG/go-sqlmock |
2.使用示例
相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头
(1)查询mock
price_policy.go
package model |
import ( |
"gorm.io/gorm" |
) |
type PricePolicy struct { |
gorm.Model |
Catogory string `gorm:"type:varchar(64)" json:"catogory" label:"收费类型"` |
Title string `gorm:"type:varchar(64)" json:"title" label:"标题"` |
Price uint64 `gorm:"type:int(5)" json:"httptest_demo" label:"价格"` |
ProjectNum uint64 `json:"project_num" label:"项目数量"` |
ProjectMember uint64 `json:"project_member" label:"项目成员人数"` |
ProjectSpace uint64 `json:"project_space" label:"每个项目空间" help_text:"单位是M"` |
PerFileSize uint64 `json:"per_file_size" label:"单文件大小" help_text:"单位是M"` |
} |
// GetAllBlog 查询所有博客信息 |
func GetAllBlog() PricePolicy { |
var allBlog PricePolicy |
DB.Find(&allBlog) |
return allBlog |
} |
// TypeBlog 根据类型查找博客 |
func TypeBlog(tyb string) PricePolicy { |
var typeBlog PricePolicy |
DB.Model(&PricePolicy{}).Where("type=?", tyb).Find(&typeBlog) |
return typeBlog |
} |
// TopBlog 置顶博客查询 |
func TopBlog(top string) PricePolicy { |
var topBlog PricePolicy |
DB.Model(&PricePolicy{}).Where("top=?", top).Find(&topBlog) |
return topBlog |
} |
price_policy_test.go
package model |
import ( |
"github.com/DATA-DOG/go-sqlmock" |
"github.com/stretchr/testify/assert" |
"gorm.io/driver/mysql" |
"gorm.io/gorm" |
"testing" |
"time" |
) |
// TestGetAllBlog GetAllBlog 函数单元测试 |
func TestGetAllBlog(t *testing.T) { |
// 步骤1:创建 sqlmock 模拟连接(内存级,无真实数据库依赖) |
// sqlmock.New() 返回 mockDB(*sql.DB)、mock(sqlmock.Sqlmock)、error |
mockSqlDB, mock, err := sqlmock.New() |
assert.NoError(t, err, "创建 sqlmock 连接失败") |
defer mockSqlDB.Close() // 测试结束关闭模拟连接 |
// 步骤2:将 sqlmock 连接适配为 GORM 可用的 DB 实例 |
// 关键:使用 gorm mysql 驱动,传入 mock 的 *sql.DB 实例 |
gormDB, err := gorm.Open(mysql.New(mysql.Config{ |
Conn: mockSqlDB, // 绑定 sqlmock 的连接 |
SkipInitializeWithVersion: true, // 跳过 MySQL 版本检测(模拟连接无需版本信息) |
}), &gorm.Config{}) |
assert.NoError(t, err, "GORM 绑定 sqlmock 连接失败") |
// 步骤3:替换全局 DB 为 mock 的 GORM DB(核心:让业务函数使用 mock 连接) |
DB = gormDB |
// 步骤4:构造模拟返回数据(与 PricePolicy 字段对应,需包含 gorm.Model 的默认字段) |
expectedPolicy := PricePolicy{ |
Model: gorm.Model{ |
ID: 1, |
CreatedAt: time.Time{}, // 测试中可忽略时间字段,若需精确匹配可赋值 time.Time 实例 |
UpdatedAt: time.Time{}, |
DeletedAt: gorm.DeletedAt{}, |
}, |
Catogory: "个人版", |
Title: "基础收费套餐", |
Price: 99, |
ProjectNum: 5, |
ProjectMember: 10, |
ProjectSpace: 1024, |
PerFileSize: 50, |
} |
// 步骤5:设置 sqlmock 预期(关键:匹配 GORM 自动生成的 SQL 语句) |
// GORM 的 Find(&allBlog) 会生成 SELECT * FROM `price_policies` 语句(表名默认是结构体小写复数) |
// 使用正则匹配,忽略无关空格和潜在的字段顺序差异 |
rows := sqlmock.NewRows([]string{ |
"id", "created_at", "updated_at", "deleted_at", |
"catogory", "title", "httptest_demo", "project_num", |
"project_member", "project_space", "per_file_size", |
}).AddRow( |
expectedPolicy.ID, expectedPolicy.CreatedAt, expectedPolicy.UpdatedAt, expectedPolicy.DeletedAt, |
expectedPolicy.Catogory, expectedPolicy.Title, expectedPolicy.Price, expectedPolicy.ProjectNum, |
expectedPolicy.ProjectMember, expectedPolicy.ProjectSpace, expectedPolicy.PerFileSize, |
) |
// 预设查询预期:匹配 GORM 生成的 SELECT 语句 |
mock.ExpectQuery("^SELECT \\* FROM `price_policies`"). |
WillReturnRows(rows) // 设置查询返回的模拟数据 |
// 步骤6:执行待测试函数 |
_ = GetAllBlog() |
// 步骤7:验证结果 |
// 关键:验证所有 sqlmock 预期都已被执行(无遗漏、无多余操作) |
assert.NoError(t, mock.ExpectationsWereMet(), "存在未满足的 sqlmock 预期") |
} |
命令行执行命令
go test -run "^TestGetAllBlog$" -cover
结果:
PS D:\wyl\workspace\go\tracer\model> go test -run "^TestGetAllBlog$" -cover |
PASS |
coverage: 13.6% of statements |
ok tracer/model 0.082s |
(2)增删改mock
这个需要注意,gorm在执行增删改动作底层使用了事务操作,所以代码中没有使用到事务时,在mock中也要mock事务操作
user.go
package model |
import ( |
"gorm.io/gorm" |
) |
// UserInfo 用户表 |
type UserInfo struct { |
gorm.Model |
UserName string `gorm:"type:varchar(32);unique" json:"user_name" label:"用户名"` |
Password string `gorm:"size:60" json:"password" label:"密码"` |
Phone string `gorm:"size:11;unique" json:"phone" label:"手机号"` |
Email string `gorm:"size:32;unique" json:"email" label:"邮箱"` |
} |
// GetAllUser 查询所有用户信息 |
func GetAllUser() (users []UserInfo, err error) { |
err = DB.Model(&UserInfo{}).Find(&users).Error |
return |
} |
func UpdateUserPhone(id int64, phone string) (err error) { |
err = DB.Model(&UserInfo{}).Where("id = ?", id).Updates(map[string]interface{}{ |
"phone": phone, |
}).Error |
return |
} |
user_test.go
package model |
import ( |
"errors" |
"github.com/DATA-DOG/go-sqlmock" |
"github.com/stretchr/testify/assert" |
"gorm.io/driver/mysql" |
"gorm.io/gorm" |
"testing" |
) |
// TestUpdateUserPhone_success 测试场景1:更新手机号【成功】- 正常更新匹配ID的用户手机号 |
func TestUpdateUserPhone_success(t *testing.T) { |
// 步骤1:创建 sqlmock 模拟连接(内存级,无真实数据库依赖) |
// sqlmock.New() 返回 mockDB(*sql.DB)、mock(sqlmock.Sqlmock)、error |
mockSqlDB, mock, err := sqlmock.New() |
assert.NoError(t, err, "创建 sqlmock 连接失败") |
defer mockSqlDB.Close() // 测试结束关闭模拟连接 |
// 步骤2:将 sqlmock 连接适配为 GORM 可用的 DB 实例 |
// 关键:使用 gorm mysql 驱动,传入 mock 的 *sql.DB 实例 |
gormDB, err := gorm.Open(mysql.New(mysql.Config{ |
Conn: mockSqlDB, // 绑定 sqlmock 的连接 |
SkipInitializeWithVersion: true, // 跳过 MySQL 版本检测(模拟连接无需版本信息) |
}), &gorm.Config{}) |
assert.NoError(t, err, "GORM 绑定 sqlmock 连接失败") |
// 步骤3:替换全局 DB 为 mock 的 GORM DB(核心:让业务函数使用 mock 连接) |
DB = gormDB |
// 测试入参 |
testID := int64(1) |
testPhone := "13800138000" |
// 核心mock断言:匹配GORM生成的update语句 |
// ^ 匹配开头 $ 匹配结尾 \? 是sql占位符的正则转义 |
mock.ExpectBegin() |
mock.ExpectExec("^UPDATE `user_infos` SET `phone`=\\?,`updated_at`=\\? WHERE id = \\? AND `user_infos`.`deleted_at` IS NULL$"). |
WithArgs(testPhone, sqlmock.AnyArg(), testID). // phone=入参值, updated_at是gorm自动填充用任意值匹配, id=入参值 |
WillReturnResult(sqlmock.NewResult(testID, 1)) // 返回执行结果:影响行数1行 |
mock.ExpectCommit() |
// 执行待测试的业务函数 |
err = UpdateUserPhone(testID, testPhone) |
// 断言:执行无错误 |
if err != nil { |
t.Errorf("更新手机号失败,预期无错误,实际错误:%v", err) |
} |
} |
