本文主要针对单元测试工具,其他工具请看专栏内其它博客。
go-sqlmock
介绍:gosqlmock是一个用于模拟数据库 /sql 驱动的库,核心作用是在不依赖真实数据库实例的情况下,对数据库相关逻辑进行单元测试,避免测试过程中操作真实数据、产生脏数据或依赖数据库服务可用性。
优点:
- 解除真实数据库依赖,保证测试独立、稳定、无脏数据
- 精准控制数据库行为,覆盖常规 / 异常全量测试场景
- 兼容
database/sql标准库和主流 ORM,无侵入式集成 - 严格验证预期行为,提升测试准确性,发现隐藏问题
- 轻量级无冗余,内存级执行,测试性能优异
- 支持正则匹配,灵活适配复杂 SQL 场景
1.安装
go get github.com/DATA-DOG/go-sqlmock
2.使用示例
相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头
(1)查询mock
price_policy.go
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
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
结果:
powershell
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
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
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)
}
}
func TestUpdateUserPhone_error(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=入参值
WillReturnError(errors.New("更新手机号失败"))
mock.ExpectRollback()
// 执行待测试的业务函数
_ = UpdateUserPhone(testID, testPhone)
}
命令行执行命令
go test -run "^TestUpdateUserPhone_success$" -cover
结果:
powershell
PS D:\wyl\workspace\go\tracer\model> go test -run "^TestUpdateUserPhone_success$" -cover
PASS
coverage: 9.1% of statements
ok tracer/model 0.082s
go test -run "^TestUpdateUserPhone_error$" -cover
结果:
go
2026/01/11 17:05:36 D:/wyl/workspace/go/tracer/model/user.go:24 更新手机号失败
[0.506ms] [rows:0] UPDATE `user_infos` SET `phone`='13800138000',`updated_at`='2026-01-11 17:05:36.624' WHERE id = 1 AND `user_infos`.`deleted_at` IS NULL
PASS
coverage: 9.1% of statements
ok tracer/model 0.086s