-
Notifications
You must be signed in to change notification settings - Fork 167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Go 单元测试 -- ORM 测试 #80
Comments
使用gormv2 , update 方法跑不起来 这是我 的代码 package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var _DB *gorm.DB
// gorm 模拟
func main() {
}
func InitDB() {
dsn := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
_DB = db
}
type User struct {
Name string `gorm:"size:255"`
Secret string `gorm:"size:255"`
gorm.Model
}
func CreateUser(user *User) error {
return _DB.Create(user).Error
}
func GetUserByNameAndPassword(name, password string) (*User, error) {
var user User
err := _DB.Where("name = ? and secret = ?", name, password).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func UpdateUserNameById(id int, name string) error {
return _DB.Model(&User{}).Where("id = ?", id).Update("name", name).Error
} gorm-mock_test.go package main
import (
"database/sql"
"database/sql/driver"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"os"
"testing"
"time"
)
var (
db *sql.DB
mock sqlmock.Sqlmock
err error
)
// TestMain 是当前 package下,最先运行的函数 常用于 初始化
func TestMain(m *testing.M) {
// 把匹配器设置成相等匹配器 ,不设置默认使用 正则 匹配
db, mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if err != nil {
panic(err)
}
open, err := gorm.Open(mysql.New(mysql.Config{SkipInitializeWithVersion: true, Conn: db}), &gorm.Config{})
if err != nil {
panic(err)
}
_DB = open
// m.Run 是调用包下面各个Test 函数的入口
os.Exit(m.Run())
}
func TestCreateUser(t *testing.T) {
type args struct {
entity *User
sqlStr string
}
tests := []struct {
name string
args args
except string
}{
{
name: "测试创建用户",
args: args{
entity: &User{
Name: "",
Secret: "",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
DeletedAt: gorm.DeletedAt{},
},
},
sqlStr: "INSERT INTO `users` (`name`,`secret`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES (?,?,?,?,?,?)",
},
},
{
name: "测试创建用户2",
args: args{
entity: &User{
Name: "",
Secret: "",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
sqlStr: "INSERT INTO `users` (`name`,`secret`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES (?,?,?,?,?,?)",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock.ExpectBegin()
mock.ExpectExec(tt.args.sqlStr).
WithArgs(tt.args.entity.Name, tt.args.entity.Secret, tt.args.entity.CreatedAt, tt.args.entity.UpdatedAt, tt.args.entity.DeletedAt, tt.args.entity.ID).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := CreateUser(tt.args.entity)
assert.Nil(t, err)
})
}
}
func TestGetUserByNameAndPassword(t *testing.T) {
type args struct {
entity *User
sqlStr string
}
tests := []struct {
name string
args args
except string
}{
{
name: "测试获取用户",
args: args{
entity: &User{
Name: "ifnk",
Secret: "ifnk",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
DeletedAt: gorm.DeletedAt{},
},
},
sqlStr: "SELECT * FROM `users` WHERE (name = ? and secret = ?) AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1",
},
},
{
name: "测试获取用户",
args: args{
entity: &User{
Name: "ddd",
Secret: "ddd",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
sqlStr: "SELECT * FROM `users` WHERE (name = ? and secret = ?) AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock.ExpectQuery(tt.args.sqlStr).
WithArgs(tt.args.entity.Name, tt.args.entity.Secret).
WillReturnRows(
// 这里要跟结果集包含的列匹配,因为查询是 SELECT * 所以表的字段都要列出来
sqlmock.NewRows([]string{"id", "name", "secret", "created_at", "updated_at"}).
AddRow(1, tt.args.entity.Name, tt.args.entity.Secret, tt.args.entity.CreatedAt, tt.args.entity.UpdatedAt))
res, err := GetUserByNameAndPassword(tt.args.entity.Name, tt.args.entity.Secret)
assert.Nil(t, err)
assert.Equal(t, tt.args.entity.Name, res.Name)
})
}
}
func TestUpdateUserNameById(t *testing.T) {
type args struct {
id int
name string
}
tests := []struct {
name string
args args
except string
}{
{name: "测试更新用户", args: args{id: 1, name: "ifnk"}, except: "UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL"},
{name: "测试更新用户2", args: args{id: 2, name: "dudu"}, except: "UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock.ExpectBegin()
mock.ExpectExec(tt.except).
WithArgs(time.Now(), tt.args.name, tt.args.id).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := UpdateUserNameById(tt.args.id, tt.args.name)
assert.Nil(t, err)
})
}
}
// 定义一个AnyTime 类型,实现 sqlmock.Argument接口
// 参考自:https://qiita.com/isao_e_dev/items/c9da34c6d1f99a112207
type AnyTime struct{}
func (a AnyTime) Match(v driver.Value) bool {
// Match 方法中:判断字段值只要是time.Time 类型,就能验证通过
// 这种方式比使用 sqlmock.AnyArg() 限制性更强一些,代码可读性也会更好
_, ok := v.(time.Time)
return ok
} 单元测试 UpdateUserNameById 过不去 报的错为
是我哪里写错了呢? |
… On Sun, Jun 19, 2022 at 3:27 PM ifnk ***@***.***> wrote:
使用gormv2 , update 方法跑不起来
这是我 的代码
gorm-mock.go
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var _DB *gorm.DB
// gorm 模拟
func main() {
}
func InitDB() {
dsn := ***@***.***(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
_DB = db
}
type User struct {
Name string `gorm:"size:255"`
Secret string `gorm:"size:255"`
gorm.Model
}
func CreateUser(user *User) error {
return _DB.Create(user).Error
}
func GetUserByNameAndPassword(name, password string) (*User, error) {
var user User
err := _DB.Where("name = ? and secret = ?", name, password).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func UpdateUserNameById(id int, name string) error {
return _DB.Model(&User{}).Where("id = ?", id).Update("name", name).Error
}
gorm-mock_test.go
package main
import (
"database/sql"
"database/sql/driver"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"os"
"testing"
"time"
)
var (
db *sql.DB
mock sqlmock.Sqlmock
err error
)
// TestMain 是当前 package下,最先运行的函数 常用于 初始化
func TestMain(m *testing.M) {
// 把匹配器设置成相等匹配器 ,不设置默认使用 正则 匹配
db, mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if err != nil {
panic(err)
}
open, err := gorm.Open(mysql.New(mysql.Config{SkipInitializeWithVersion: true, Conn: db}), &gorm.Config{})
if err != nil {
panic(err)
}
_DB = open
// m.Run 是调用包下面各个Test 函数的入口
os.Exit(m.Run())
}
func TestCreateUser(t *testing.T) {
type args struct {
entity *User
sqlStr string
}
tests := []struct {
name string
args args
except string
}{
{
name: "测试创建用户",
args: args{
entity: &User{
Name: "",
Secret: "",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
DeletedAt: gorm.DeletedAt{},
},
},
sqlStr: "INSERT INTO `users` (`name`,`secret`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES (?,?,?,?,?,?)",
},
},
{
name: "测试创建用户2",
args: args{
entity: &User{
Name: "",
Secret: "",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
sqlStr: "INSERT INTO `users` (`name`,`secret`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES (?,?,?,?,?,?)",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock.ExpectBegin()
mock.ExpectExec(tt.args.sqlStr).
WithArgs(tt.args.entity.Name, tt.args.entity.Secret, tt.args.entity.CreatedAt, tt.args.entity.UpdatedAt, tt.args.entity.DeletedAt, tt.args.entity.ID).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := CreateUser(tt.args.entity)
assert.Nil(t, err)
})
}
}
func TestGetUserByNameAndPassword(t *testing.T) {
type args struct {
entity *User
sqlStr string
}
tests := []struct {
name string
args args
except string
}{
{
name: "测试获取用户",
args: args{
entity: &User{
Name: "ifnk",
Secret: "ifnk",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
DeletedAt: gorm.DeletedAt{},
},
},
sqlStr: "SELECT * FROM `users` WHERE (name = ? and secret = ?) AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1",
},
},
{
name: "测试获取用户",
args: args{
entity: &User{
Name: "ddd",
Secret: "ddd",
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
sqlStr: "SELECT * FROM `users` WHERE (name = ? and secret = ?) AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock.ExpectQuery(tt.args.sqlStr).
WithArgs(tt.args.entity.Name, tt.args.entity.Secret).
WillReturnRows(
// 这里要跟结果集包含的列匹配,因为查询是 SELECT * 所以表的字段都要列出来
sqlmock.NewRows([]string{"id", "name", "secret", "created_at", "updated_at"}).
AddRow(1, tt.args.entity.Name, tt.args.entity.Secret, tt.args.entity.CreatedAt, tt.args.entity.UpdatedAt))
res, err := GetUserByNameAndPassword(tt.args.entity.Name, tt.args.entity.Secret)
assert.Nil(t, err)
assert.Equal(t, tt.args.entity.Name, res.Name)
})
}
}
func TestUpdateUserNameById(t *testing.T) {
type args struct {
id int
name string
}
tests := []struct {
name string
args args
except string
}{
{name: "测试更新用户", args: args{id: 1, name: "ifnk"}, except: "UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL"},
{name: "测试更新用户2", args: args{id: 2, name: "dudu"}, except: "UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock.ExpectBegin()
mock.ExpectExec(tt.except).
WithArgs(time.Now(), tt.args.name, tt.args.id).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := UpdateUserNameById(tt.args.id, tt.args.name)
assert.Nil(t, err)
})
}
}
// 定义一个AnyTime 类型,实现 sqlmock.Argument接口
// 参考自:https://qiita.com/isao_e_dev/items/c9da34c6d1f99a112207
type AnyTime struct{}
func (a AnyTime) Match(v driver.Value) bool {
// Match 方法中:判断字段值只要是time.Time 类型,就能验证通过
// 这种方式比使用 sqlmock.AnyArg() 限制性更强一些,代码可读性也会更好
_, ok := v.(time.Time)
return ok
}
单元测试 UpdateUserNameById 过不去
报的错为
=== RUN TestUpdateUserNameById/测试更新用户
2022/06/19 15:15:49 /home/ifnk/proj/new_go_stu/go-unit-test/gorm-mock/gorm-mock.go:44 ExecQuery 'UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL', arguments do not match: argument 0 expected [time.Time - 2022-06-19 15:15:49.902647511 +0800 CST m=+0.002770607] does not match actual [string - ifnk]; call to Rollback transaction, was not expected, next expectation is: ExpectedExec => expecting Exec or ExecContext which:
- matches sql: 'UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL'
- is with arguments:
0 - 2022-06-19 15:15:49.902647511 +0800 CST m=+0.002770607
1 - ifnk
2 - 1
- should return Result having:
LastInsertId: 1
RowsAffected: 1
[0.112ms] [rows:0] UPDATE `users` SET `name`='ifnk',`updated_at`='2022-06-19 15:15:49.903' WHERE id = 1 AND `users`.`deleted_at` IS NULL
gorm-mock_test.go:173:
Error Trace: gorm-mock_test.go:173
Error: Expected nil, but got: &fmt.wrapError{msg:"ExecQuery 'UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL', arguments do not match: argument 0 expected [time.Time - 2022-06-19 15:15:49.902647511 +0800 CST m=+0.002770607] does not match actual [string - ifnk]; call to Rollback transaction, was not expected, next expectation is: ExpectedExec => expecting Exec or ExecContext which:\n - matches sql: 'UPDATE `users` SET `name`=?,`updated_at`=? WHERE id = ? AND `users`.`deleted_at` IS NULL'\n - is with arguments:\n 0 - 2022-06-19 15:15:49.902647511 +0800 CST m=+0.002770607\n 1 - ifnk\n 2 - 1\n - should return Result having:\n LastInsertId: 1\n RowsAffected: 1", err:(*errors.errorString)(0xc000233b20)}
Test: TestUpdateUserNameById/测试更新用户
--- FAIL: TestUpdateUserNameById/测试更新用户 (0.00s)
是我哪里写错了呢?
—
Reply to this email directly, view it on GitHub
<#80 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACDCUYD3CT6IP2XCNVKCVE3VP3DWLANCNFSM5V5G3JAA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
在 Go 单元测试这个系列的第二部分 数据库的Mock测试 中我们介绍了用
go-sqlmock
给数据库的 CRUD 操作做Mock 测试的方法,不过里面只是讲解了一下怎么对原生的database/sql
执行的 SQL 进行 Mock 测试。前言
真实的开发场景下我们的项目一般都会使用 ORM ,而不是原生的
database/sql
来完成数据库操作。在很多使用ORM工具的场景下,也可以使用go-sqlmock
库 Mock数据库操作进行测试,今天这篇内容我就以 GORM 为例,讲解怎么给项目中的 ORM 数据库操作做单元测试。项目准备
为了场景足够真实,我用 2020 年我更新的 「Go Web 编程入门」项目中的例子给大家演示怎么为使用了 GORM 的 DAO 层逻辑做 Mock 测试。
在这个例子中我们有一个与 users 表
以及几个使用 User 的 DAO 函数:
接下来我们就用
go-sqlmock
工具给这几个 DAO 函数做一下 Mock 测试。初始化测试工作
首先我们需要做一下测试的初始化工作,主要是设置Mock的DB连接,因为要给三个方法做Mock测试,最简单的办法是在三个方法里每次都初始化一遍 Mock 的 DB 连接,不过这么做实在是显得有点蠢,这里给大家再介绍一个小技巧。
Go 的测试支持在包内优先执行一个
TestMain(m *testing.M)
函数,可以在这里为 package 下所有测试做一些初始化的工作。下面是我们为本次测试做的初始化工作。
在这个初始化函数里我们创建一个
sqlmock
的数据库连接db
和mock
对象,mock
对象管理db
预期要执行的SQL。让sqlmock 使用 QueryMatcherEqual 匹配器,该匹配器把mock.ExpectQuery 和 mock.ExpectExec 的参数作为预期要执行的SQL语句跟实际要执行的SQL进行相等比较。
m.Run 是调用包下面各个Test函数的入口。
准备工作做好了,下面正式对 DAO 操作进行Mock测试。
对Create进行Mock测试
首先对 GORM 的Create 方法进行Mock测试。
因为 sqlmock 使用的是 QueryMatcherEqual 匹配器,所以,预期会执行的 SQL 语句必须精确匹配要执行的SQL(包括符号和空格)。
这个SQL怎么获取呢?其实我们先随便写一个SQL,执行一次测试,在报错信息里就会告知
CreateUser
操作在写表时 GORM 真正要执行的 SQL 啦, 也可以通过GORM提供的Debug()
方法获取到。比如运行一下下面这个设置了
Debug()
的创建用户操作,GORM就会打印出执行的语句。我们执行下这个测试
go test -v -run TestCreateUserMock -------- === RUN TestCreateUserMock --- PASS: TestCreateUserMock (0.00s) PASS ok golang-unit-test-demo/sqlmock_gorm_demo 0.301s
可以看到,测试函数执行成功,我们还可以故意把SQL改成,做一下反向测试,这个就留给你们自己联系啦,结合上表格测试分别做一下正向和反向单元测试。
Get 操作的Mock测试
GORM 的查询操作的Mock测试跟Create类似。
这里就不在文章里运行演示啦,有兴趣的自己把代码拿下来试一下。
Update 操作的Mock测试
GORM的Update操作我没有测试成功,我这里发出来原因
运行测试后,会有下面的报错信息:
GORM 在UPDATE 的时候会自动更新updated_at 字段为当前时间,与这里withArgs传递的 time.Now() 参数不一致(毫秒级的差距也不行)。
这种情况可以选择在 Mock 要执行的更新 SQL 时给
update_at
字段的值设置成sqlmock.AnyArg()
,就能测试通过了,上面的 UPDATE 测试改成下面这样:这个方法是
sqlmock
提供的用来断言匹配任意字段值的一个特殊的类型。在其注释里也有说明,尤其适合time.Time
类型字段的断言。当然使用
sqlmock.AnyArg()
在测试代码的可读性上,以及严谨性上都会差点意思,因为如果真实执行的 SQL 中如果updated_at
字段设置的值不是time.Time
类型的,使用sqlmock.AnyArg()
做断言匹配是无法测出来的。所以我们也可以选择实现自己定义一个
AnyTime
类型,让它实现sqlmock.Argument
接口,比如下面的示例:在
AnyTime
类型实现接口定义的Match
方法的逻辑是:判断字段值只要是time.Time 类型,就能验证通过。这种方式比使用sqlmock.AnyArg()
限制性更强一些,代码可读性也会更好。总结
这篇内容我们把ORM的 Mock 测试做了一个讲解,这个也是我在学习 Go 单元测试时自己的思考,希望学习到的这些技能能在项目中真实用到。
因为文章中的示例,是以我之前的Go Web 编程教程里的项目里做的测试,源码我也打包更新到了Go Web 编程的项目中啦,公众号私信 gohttp15 就能获得。
如果你觉得有用,可以点赞、在看、分享给更多人,谢谢各位的支持,后面会与时俱进再搞一篇 Go 1.18 Fuzing 测试的使用介绍。
相关阅读
Go 单元测试--数据库的Mock测试
The text was updated successfully, but these errors were encountered: