Gomock

什么是gomock

gomock是Go官方提供的mock框架,提供打桩stub和验证调用的功能,它能对代码中的接口类型进行mock,方便编写单元测试

mockgen工具用于针对接口生成mock对象代码(不能mock单独的函数或方法,必须是接口)

gomock通过mockgen命令生成包含mock对象的 .go 文件,生成的mock对象具备mock+stub的强大功能

安装和使用mockgen

1
2
3
go get github.com/golang/mock/gomock
go insatll github.com/golang/mock/mockgen
//安装完成后可以在命令行中直接使用mockgen命令(需要将$GOPATH/bin添加到PATH环境变量)

1
mockgen -source xxx.go [options]

image-20220504111610258

flags

  • -source:包含要mock的接口的文件

  • -destination:生成的源代码写入的文件。如果不设置此项,代码将打印到标准输出

  • -package:用于生成的模拟类源代码的包名。如果不设置此项包名默认在原包名前添加mock_前缀

mock和stub

下面使用日常开发的案例进行演示,如博客项目中对文章数据表进行增删改查

想要测试的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func UpdateArticle(c *gin.Context, db dao.ArticleDao, req UpdateArticleREQ) error {
article := model.Article{
BaseModel: model.BaseModel{
ID: req.ID,
},
Title: req.Title,
Content: req.Content,
Desc: req.Desc,
}
if err := db.Update(article); err != nil { //这一步是需要连接数据库进行相关文章的更新,但是在单元测试时不想连接数据库,则需要进行打桩stub
log.Errorf("save article err %v", err)
common.ResErrCode(c, common.ErrService)
return err
}
log.Infof("update article %s success", article.Title)
common.ResSuccessMsg(c)
return nil
}

首先使用文章数据表的数据库操作接口

因为mock是接口类型的测试框架,所以需要构造测试单元函数的相应接口

1
2
3
4
5
6
7
type ArticleDao interface {
Get(id uint) (model.Article, error)
List(opt common.ListOption) ([]model.Article, error)
Create(article model.Article) error
Update(article model.Article) error
Delete(article model.Article) error
}

使用mockgen工具生成相应的mock代码

1
mockgen -source article.go -destination mock/mock_article.go

生成的代码不需要对它们进行编辑,只需要在单元测试时调用它们

单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
func TestArticle_Update(t *testing.T) {
//创建gomock控制器,用来记录后续的操作信息
ctrl := gomock.NewController(t)
defer ctrl.Finish()

//调用mockgen生成代码中的NewMockArticleDao()方法
mockDao := mocks.NewMockArticleDao(ctrl)
//打桩(stub)
mockDao.EXPECT().Update(gomock.Any()).Return(nil)

type args struct {
ctx *gin.Context
db dao.ArticleDao
req UpdateArticleREQ
}
tests := []struct { //表格驱动测试方法(Table Driven Tests)
name string
args args
want error
}{
{
name: "default",
args: args{
ctx: &gin.Context{},
db: mockDao, //注入mockDao
req: UpdateArticleREQ{
ID: 1,
Title: "test",
Content: "this is test",
},
},
want: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := UpdateArticle(test.args.ctx, test.args.db, test.args.req)//单元测试时将mockDao传入替换相应接口
if !reflect.DeepEqual(err, test.want) {
t.Errorf("UpdateArticle() = %v, want %v", err, test.want)
}
})
}
}

打桩(stub)

软件测试中的打桩是指用一些代码(桩stub)代替目标代码,通常用来屏蔽或补齐业务逻辑中的关键代码方便进行单元测试。

  • 屏蔽:不想在单元测试用引入数据库连接等重资源

  • 补齐:依赖的上下游函数或方法还未实现

上面的单元测试中引入打桩代码,目的是为了在测试 UpdateArticle( ) 中不想连接数据库并进行操作

1
mockDao.EXPECT().Update(gomock.Any()).Return(nil)

结合最开始文章表相关数据库操作接口,表示在测试 UpdateArticle( ) 时如果遇到 Update(article model.Article) error 的函数,无论传入任何参数 (gomock.Any()) 都返回nil (Return(nil))

1
2
3
type ArticleDao interface {
Update(article model.Article) error
}

参数

1
2
3
mockDao.EXPECT().Update(gomock.Any()).Return(nil)
mockDao.EXPECT().Update(gomock.Not(value)).Return(nil)
mockDao.EXPECT().Update(gomock.Nil()).Return(nil)
  • gomock.Any( ):任意参数

  • gomock.Not(value):非value的参数

  • gomock.Nil( ):空值的参数

  • gomock.Eq(value):等于value的参数

返回值

1
2
3
4
mockDao.EXPECT().Update(gomock.Any()).DoAndReturn(func(key string)(error) {
t.Logf("input key is %v\n", key)
return nil
})
  • Return( ):返回指定值
  • Do(func):执行操作,忽略返回值
  • DoAndReturn(func):执行并返回指定值

调用次数

使用gomock工具mock的方法都会有期望被调用的次数,默认每个mock方法只允许被调用一次

1
mockDao.EXPECT().Update(gomock.Any()).Return(nil).Times(1) 
  • Times() 指定mock方法被调用的次数
  • MaxTimes() 最大次数
  • MinTimes() 最小次数
  • AnyTimes() 任意次数(包括 0 次)