Gorm V2 mock 测试

2022年6月4日 420点热度 0人点赞 0条评论

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框架的逻辑进行单元测试,希望大家有所收获

36950Gorm V2 mock 测试

这个人很懒,什么都没留下

文章评论