gorm v2 mock测试
平时开发中会经常用到各种数据库,做单元测试时又不想真正的连接数据库。今天介绍如何在基于gorm v2做单元测试
go-sqlmock
今天的主角就是go-sqlmock的工具,它是一个实现sql/drive mock库,不需要建立真正的数据库连接就可以在测试中模拟任何 sql 驱动程序的行为。可以很方便的在编写单元测试的时候mock sql语句的执行结果。
安装
go get github.com/DATA-DOG/go-sqlmock
示例
新建一个simple_sqlmock.go文件,添加变量DB 和 初始化DB的方法
package simple
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
)
var DB *gorm.DB
func Init() {
var err error
DB, err = gorm.Open(mysql.Open("dsn"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // 日志配置
})
if err != nil {
panic(err)
}
}
写一个简单用户SysUser
package simple
type SysUser struct {
Id uint64 `gorm:"primarykey"`
UserName string `gorm:"column:username;type:varchar(255);default:'';not null;comment:账号;index:idx_name"`
Password string `gorm:"column:password;type:varchar(1024);default:'';not null;comment:密码"`
Age int `gorm:"column:age;type:int(10);defalut:0;not null;comment:年龄"`
}
func (SysUser) TableName() string {
return "sys_users"
}
添加对用户增删改查的方法
package simple
func AddUser(u *SysUser) error {
if u == nil {
return nil
}
err := DB.Model(&SysUser{}).Create(u).Error
if err != nil {
return err
}
return nil
}
func UpdateUser(name string, age int) error {
err := DB.Model(&SysUser{}).Where("username=?", name).UpdateColumn("age", age).Error
if err != nil {
return err
}
return nil
}
func QueryUser(id uint64) (*SysUser, error) {
if id == 0 {
return nil, nil
}
var user SysUser
err := DB.Model(&SysUser{}).Where("id = ?", id).Scan(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func DelUser(id uint64) error {
if id == 0 {
return nil
}
err := DB.Model(&SysUser{}).Delete(&SysUser{}, id).Error
if err != nil {
return err
}
return nil
}
再新建一个simple_sqlmock_test 文件对用户增删改查的方法做单元测试,发现对每一个测试方法都要初始化一遍mock DB连接,做起来比较繁琐。这里有个小技巧, 可以把初始化的工作放到TestMain中,go的测试支持会优先执行这个方法
package simple
import (
"database/sql"
"github.com/DATA-DOG/go-sqlmock"
"github.com/smartystreets/goconvey/convey"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"reflect"
"testing"
)
var (
err error
mysqlMock sqlmock.Sqlmock
sqldb *sql.DB
)
func TestMain(m *testing.M) {
sqldb, mysqlMock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if err != nil {
panic(err)
}
DB, err = gorm.Open(mysql.New(mysql.Config{
Conn: sqldb,
SkipInitializeWithVersion: true,
}), &gorm.Config{})
m.Run()
}
初始化做的主要工作:
-
• 创建一个 sqlmock 的数据库连接 db 和 mock对象,mock对象管理 db 预期要执行的SQL。
-
• 让sqlmock 使用 QueryMatcherEqual 匹配器,该匹配器把mock.ExpectQuery 和 mock.ExpectExec 的参数作为预期要执行的SQL语句跟实际要执行的SQL进行相等比较。还有其他的匹配器,比如正则匹配QueryMatcherRegexp
-
• m.Run 是调用包下面各个Test函数的入口,会优先执行。
使用上次讲的gotests 自动生成单元测试的方法,测试代码如下:
func TestAddUser(t *testing.T) {
type args struct {
u *SysUser
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
{
name: "add user case1",
args: args{u: &SysUser{
UserName: "admin1",
Password: "123456",
Age: 10,
}},
wantErr: false,
},
{
name: "add user case2",
args: args{u: &SysUser{
UserName: "admin2",
Password: "123456",
Age: 20,
}},
wantErr: false,
},
}
mysqlMock.ExpectBegin()
mysqlMock.ExpectExec("INSERT INTO `sys_users` (`username`,`password`,`age`) VALUES (?,?,?)").
WithArgs("admin1", "123456", 10).
WillReturnResult(sqlmock.NewResult(1, 1))
mysqlMock.ExpectCommit()
mysqlMock.ExpectBegin()
mysqlMock.ExpectExec("INSERT INTO `sys_users` (`username`,`password`,`age`) VALUES (?,?,?)").
WithArgs("admin2", "123456", 20).
WillReturnResult(sqlmock.NewResult(2, 1))
mysqlMock.ExpectCommit()
convey.Convey("Test Add User Case", t, func() {
for _, tt := range tests {
convey.Convey(tt.name, func() {
if err := AddUser(tt.args.u); (err != nil) != tt.wantErr {
t.Errorf("AddUser() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
})
}
func TestUpdateUser(t *testing.T) {
type args struct {
Name string
Age int
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
{
name: "update user case1",
args: args{
Name: "james",
Age: 10,
},
wantErr: false,
},
{
name: "update user case2",
args: args{
Name: "jordan",
Age: 23,
},
wantErr: false,
},
}
mysqlMock.ExpectBegin()
mysqlMock.ExpectExec("UPDATE `sys_users` SET `age`=? WHERE username=?").
WithArgs(10, "james").
WillReturnResult(sqlmock.NewResult(1, 1))
mysqlMock.ExpectCommit()
mysqlMock.ExpectBegin()
mysqlMock.ExpectExec("UPDATE `sys_users` SET `age`=? WHERE username=?").
WithArgs(23, "jordan").
WillReturnResult(sqlmock.NewResult(1, 1))
mysqlMock.ExpectCommit()
convey.Convey("Test Update User Case", t, func() {
for _, tt := range tests {
convey.Convey(tt.name, func() {
if err := UpdateUser(tt.args.Name, tt.args.Age); (err != nil) != tt.wantErr {
t.Errorf("UpdateUser() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
})
}
func TestQueryUser(t *testing.T) {
type args struct {
id uint64
}
tests := []struct {
name string
args args
want *SysUser
wantErr bool
}{
// TODO: Add test cases.
{
name: "query user case1",
args: args{id: 1},
want: &SysUser{
Id: 1,
UserName: "james",
Password: "123456",
Age: 10,
},
wantErr: false,
},
{
name: "query user case2",
args: args{id: 2},
want: &SysUser{
Id: 2,
UserName: "jordan",
Password: "123456",
Age: 23,
},
wantErr: false,
},
}
mysqlMock.ExpectQuery("SELECT * FROM `sys_users` WHERE id = ?").
WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "password", "age"}).AddRow(1, "james", "123456", 10))
mysqlMock.ExpectQuery("SELECT * FROM `sys_users` WHERE id = ?").
WithArgs(2).
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "password", "age"}).AddRow(2, "jordan", "123456", 23))
convey.Convey("Test Query User Case", t, func() {
for _, tt := range tests {
convey.Convey(tt.name, func() {
got, err := QueryUser(tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("QueryUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("QueryUser() = %v, want %v", got, tt.want)
}
})
}
})
}
func TestDelUser(t *testing.T) {
type args struct {
id uint64
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
{
name: "del user case1",
args: args{id: 1},
wantErr: false,
},
{
name: "del user case2",
args: args{id: 2},
wantErr: false,
},
}
mysqlMock.ExpectBegin()
mysqlMock.ExpectExec("DELETE FROM `sys_users` WHERE `sys_users`.`id` = ?").WithArgs(1).WillReturnResult(sqlmock.NewResult(1, 1))
mysqlMock.ExpectCommit()
mysqlMock.ExpectBegin()
mysqlMock.ExpectExec("DELETE FROM `sys_users` WHERE `sys_users`.`id` = ?").WithArgs(2).WillReturnResult(sqlmock.NewResult(1, 1))
mysqlMock.ExpectCommit()
convey.Convey("Test Del User Case", t, func() {
for _, tt := range tests {
convey.Convey(tt.name, func() {
if err := DelUser(tt.args.id); (err != nil) != tt.wantErr {
t.Errorf("DelUser() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
})
}
执行go test
go test -v simple_sqlmock.go simple_sqlmock_test.go
=== RUN TestAddUser
Test Add User Case
add user case1
add user case2
0 total assertions
--- PASS: TestAddUser (0.00s)
=== RUN TestUpdateUser
Test Update User Case
update user case1
update user case2
0 total assertions
--- PASS: TestUpdateUser (0.00s)
=== RUN TestQueryUser
Test Query User Case
query user case1
query user case2
0 total assertions
--- PASS: TestQueryUser (0.00s)
=== RUN TestDelUser
Test Del User Case
del user case1
del user case2
0 total assertions
--- PASS: TestDelUser (0.00s)
PASS
ok command-line-arguments 0.015s
能看到测试结果都通过了,也可以做一些反向测试,故意改错一些参数,提示sql 语句不匹配等错误导致测试fail
总结
学习并使用了go-sqlmock工具对使用gorm框架的逻辑进行单元测试,希望大家有所收获
文章评论