gorm使用sqlmock进行sqlite的数据库CRUD单元测试
完整Demo代码
本文将会围绕该demo进行讲解:
https://github.com/Lichmaker/gorm-sqlite-sqlmock-demo
mock的作用
在单元测试中,我们不希望动到实际的资源,例如DB、第三方HTTP API等。
当我们在单元测试时遇到数据库操作,则会用到mock
- 分析所有sql语句, 如果没有执行到预设的语句则会返回错误
- mock出语句的结果来达到数据库模拟的效果
mock 初始化
sqlmock
是一个强大的 mock 工具包 https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock
在gorm的初始化在引入驱动时候可以把 sqlmock
生成出来的 *sql.DB
注入到gorm中。
但 gorm 的 sqlite 驱动并没有提供配置项,所以我fork了下来自己修改,可以直接用来mock driver。 https://github.com/lichmaker/sqlite-mock
// bootstrap.go
package main
import (
"database/sql"
sqlitemock "github.com/lichmaker/sqlite-mock"
"gorm.io/gorm"
)
var DB *gorm.DB
func SetupDatabaseMock(mockDB *sql.DB) {
myMock := sqlitemock.Open(mockDB)
dbSqlite, err := gorm.Open(myMock, &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
panic("failed to connect mock database :" + err.Error())
}
DB = dbSqlite
}
如果是使用 mysql 则可以直接使用官方的mysql驱动,传入配置项,如下:
// bootstrap.go
package main
import (
"database/sql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func SetupDatabaseMock(mockDB *sql.DB) {
myMock := mysql.New(mysql.Config{
Conn: mockDB,
SkipInitializeWithVersion: true,
})
dbSqlite, err := gorm.Open(myMock, &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
panic("failed to connect mock database :" + err.Error())
}
DB = dbSqlite
}
简单举例一个Model和数据库操作
代码我不贴了,可以到demo里看 model.go
。 我们重点看单元测试里的
单元测试初始化
在单元测试中,我们使用 TestMain
执行初始化
// model_test.go
//......
var (
mock sqlmock.Sqlmock
)
func TestMain(m *testing.M) {
var db *sql.DB
var err error
db, mock, err = sqlmock.New()
if err != nil {
panic(err)
}
SetupDatabaseMock(db)
m.Run()
}
//......
这里是new一个 mock ,然后调用之前的 db bootstrap代码,完成gorm的初始化。 这样在该test文件中所有的测试用例都可以通过 mock
变量来进行mock操作。
对Exec进行mock
对于插入、删除等非query操作,可以通过 ExpectExec
来验证语句
// model_test.go
func TestInsert(t *testing.T) {
menuModel := Menu{}
menuModel.ParentId = 0
menuModel.Title = "test"
menuModel.Name = "testName"
menuModel.Sort = 1
menuModel.CreatedAt = time.Now()
menuModel.UpdatedAt = time.Now()
mock.ExpectExec("INSERT INTO `menus`").WithArgs(menuModel.ParentId, menuModel.Title, menuModel.Name, menuModel.Sort, menuModel.Route, menuModel.Component, menuModel.Icon, AnyTime{}, AnyTime{}, nil).WillReturnResult(sqlmock.NewResult(0, 0))
// now we execute our method
if err := Insert(&menuModel); err != nil {
t.Errorf("models.NewTMenuModel().Insert() error : %s", err.Error())
}
// we make sure that all expectations were met
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
请记住,需要在调用数据库操作前,通过 mock 进行语句的预设。上代码中,会通过正则匹配到 ExpectExec
中的语句,然后验证 WithArgs
传入的数据是否与实际相符。
最后的 WillReturnResult
则会预设好该语句执行的结果,达到模拟数据库操作的目的。
这里就完成了一个 insert 语句的测试。
对Query进行mock
对于查询的语句,mock的时候需要传入预设的数据,代码量稍微有点多
// model_test.go
func TestGetAllByParentId(t *testing.T) {
mock.ExpectQuery("SELECT (.+) FROM `menus`").WithArgs(0).WillReturnRows(sqlmock.NewRows([]string{
"id", "parentId", "title", "name", "sort", "route", "component", "icon", "createdAt", "updatedAt", "deletedAt",
}).AddRow(
1, 0, "menu1", "一级菜单1", 1, "/index", "", "", time.Now(), time.Now(), nil,
).AddRow(
2, 0, "menu2", "一级菜单2", 2, "/index", "", "", time.Now(), time.Now(), nil,
))
data, err := GetAllByParentId(0)
if err != nil {
t.Errorf("查询错误 %s", err)
}
if len(data) != 2 {
t.Errorf("查询数据异常!")
}
}
WillReturnRows
会把预设好的数据赋值给这一条查询。需要注意的是不单止 ExpectQuery
通过正则匹配到sql语句,还要传入的参数一致。 例如代码中 GetAllByParentId(0)
传入的是0,则 WithArgs(0)
也要传入0。 可以随意修改一个值再进行测试,可以看到错误的结果。
就这样就完成了一次数据库查询的验证和mock, 在 data, err := GetAllByParentId(0)
的 data
中就可以拿到之前预设好的mock数据,进行其他测试。
其他数据库操作mock
可以通过查阅sqlmock官方文档,有详细的说明 https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。