当前位置: 首页 > news >正文

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)
}
}
http://www.jsqmd.com/news/1093076/

相关文章:

  • AI数字人平台热门十三问|必火AI数字人全维度专业解答
  • 如何高效优化电子书阅读体验:Kindle Comic Converter的完整漫画转换方案
  • 卡梅德生物技术快报|羊驼纳米抗体文库筛选实操全流程:天然 / 合成文库构建与淘选参数汇总
  • Windows虚拟显示器终极指南:Parsec VDD免费开源解决方案
  • 从 0 开始学 Python:装好环境,写一下demo实例
  • Kali Linux下使用apk2url从APK提取URL与IP的实战指南
  • 高效智能的网盘直链下载解决方案:一站式专业级工具LinkSwift深度解析
  • GPU硬件故障排查终极指南:5分钟完成显卡内存稳定性检测
  • 收藏!小白程序员必看:如何将大模型Agent从Demo成功落地工程实践?
  • 2026年大模型知识库优化实战?GEO策略如何重塑TOB品牌获客新路径
  • 收藏!小白程序员必看:一文搞懂AI Agent核心原理与实战代码
  • [Android] iVCam(手机变电脑摄像头)专业版
  • 01 TCP 协议是流式协议
  • Lean 4实战指南:5个步骤掌握下一代定理证明编程语言
  • Fatal error: require(): Failed opening required...” 以及如何彻底避免它再次出现
  • 2026年AI Agent大爆发!小白程序员必看:收藏这份从入门到精通指南,抓住时代红利!
  • 5个技巧轻松解决经典游戏兼容问题:开源dxwrapper完全指南
  • Vibe Coding:说人话就能做软件,超简单开发流程全讲明白
  • Netty 高性能网络编程:从零构建高并发服务器
  • 【TSP问题】基于帝企鹅算法AFO求解单仓库多旅行商问题MTSP附Matlab代码
  • XSS防御实战:从同源策略到CSP的纵深安全体系构建
  • Kafka2.4-Windows安装教程
  • 无需同看同一张图:跨被试神经表征对齐的VAE新范式
  • 一文吃透Java IO流!从底层原理到实战代码(新手必看)
  • 只有 B 级能力的大模型,怎么干出 A 级的活?
  • 续流二极管:电机断电瞬间的“高压泄洪道”
  • 容器化 Java 应用 CPU 使用率监控口径解析:node exporter vs cAdvisor vs JMX
  • 工程项目过程留痕管理的3个断点与5款软件选型对比
  • 02 状态(State)
  • 多发射器识别技术(SMEI)在无线通信安全中的应用