完整Demo代码

本文将会围绕该demo进行讲解:
https://github.com/Lichmaker/gorm-sqlite-sqlmock-demo

mock的作用

在单元测试中,我们不希望动到实际的资源,例如DB、第三方HTTP API等。

当我们在单元测试时遇到数据库操作,则会用到mock

  1. 分析所有sql语句, 如果没有执行到预设的语句则会返回错误
  2. 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

文章目录