单元测试-go-sqlmock

项目demo地址:go-test

本文主要针对单元测试工具,其他工具请看专栏内其它博客。

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

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
相关推荐
苏琢玉1 小时前
用 Go 实现一个可长期运行的 GitHub Webhook 服务实践
golang·github
草根大哥2 小时前
AI编程实践-homex物业管理平台(Go + Vue3 + MySQL 多租户落地)
mysql·golang·vue·ai编程·gin·物业管理系统·多租户
Maguyusi2 小时前
go 批量生成 c++与lua的proto文件
开发语言·后端·golang·protobuf
139的世界真奇妙2 小时前
工作事宜思考点
经验分享·笔记·golang·go
jiuweiC4 小时前
kafka重平衡问题-golang
分布式·golang·kafka
jdbcaaa4 小时前
Go 语言 runtime 包的使用与注意事项
开发语言·后端·golang·runtime
王中阳Go13 小时前
从夯到拉,锐评9个Go Web框架
开发语言·golang
Grassto13 小时前
16 Go Module 常见问题汇总:依赖冲突、版本不生效的原因
golang·go·go module
Tony Bai13 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php