mockery v2的介绍和使用

1. 前言

由于项目时间比较紧, 我本来是没有打算写一篇文章来介绍mockery的, 但是无奈网上介绍mockery的文章比数量上较少(截至2023-04-27), 而且很多文章都过期了.
一方面由于golang更新比较快, 网上解释使用go get 安装mockery的, 到了go 1.6以后都安装不了. 另一方面mockery自身更新也比较快, 很多文章介绍的一些用法在新的版本中已经不灵了, 比如生成mock对象的命令选项-name已经调整为--name, -dir的意义也发生了变化等等, 出现了很多差异的地方.

所以本着稳扎稳打的原则, 不得不放慢脚步, 停下来把golang mock这一块的知识库补充完整.

2. mockery介绍

Mockery是一个用于生成Golang接口的Mock的工具. Mockery可以帮助您在测试期间模拟依赖, 以便更轻松地测试代码. Mockery v2是Mocker的最新版本.

2.1. mockery 各版本之间的区别

Mockery v1是Mockery的最初版本, 它支持生成带有单个返回值的函数和方法的Mock. Mockery v2和v3支持生成带有多个返回值的函数和方法的Mock, Mockery v3还支持生成带有可变参数的函数和方法的Mock.

另外Mockery v2的CLI在v1的基础上做了一些增强, 以下是Mockery v2新增的一些命令和选项:

  • –version: 显示Mockery的版本号.
  • –debug: 启用调试模式, 以便在生成Mock时输出更多信息.
  • –all: 生成所有接口的Mock, 而不仅仅是在命令行中指定的接口.
  • –recursive: 递归查找指定目录中的所有接口, 并生成它们的Mock.
  • –output: 指定生成Mock的输出目录.
  • –Case: 指定生成Mock时使用的命名约定(例如, snake_case或camelCase)

此外, 可以在当前目录中创建一个名为.mockery.yml的文件, 该文件包含Mockery的默认配置选项. 您可以编辑此文件以自定义Mockery的行为和输出.

例如您可以使用.mockery.yml文件来指定生成Mock时使用的命名规范, 包名, 注释等. 你还可以使用.mockery.yml文件来指定要生成Mock的接口和结构体名称, 以及要生成Mock的目录和文件名.
在V2中我们可以将一些运行mockery时需要指定的选项配置到.mockery

相对于Mockery v2而言, Mockery V3对Golang新版本的一些新特性支持更好一些, 例如:
支持Go 1.17中引入的新特性, 如泛型, 嵌入式接口, 以及Go 1.18中引入的新特性泛型类型参数, 嵌入式结构体, 嵌入式接口和结构体的混合使用, 类型别名等等.

3. 安装Mockery

安装mockery比较简单. 在Golang 1.16及以上的版本需要使用go install 安装prebuilt(也就是binary的程序)的Mockery工具,
如果使用的是golang 1.16以前的版本仍然使用go get 来安装.

3.1. go install

1
go install github.com/vektra/mockery/v2@v2.25.0

这里我安装的是mockery v2当前最新版本2.25.0版本, 版本信息可以在Mockery的release notes页面找到

3.2. Docker

Mockery也可以结合docker使用

下载docker image

1
docker pull vektra/mockery

使用Mockery生成Mock

1
docker run -v "$PWD":/src -w /src vektra/mockery --all

3.3. Homebrew

在macOS上可以使用Hombrew来安装, 安装方法如下:

1
2
brew install mockery
brew upgrade mockery

3.4. Mockery CLI的使用

前面我们讲了Mockery是一个生成Mock的工具, 那么如何使用它呢, 这里就讲一讲Mockery CLI的用法.

讲解的过程中我们遵循由浅入深的规则. 先从简单的示例开始.

3.4.1. 为某个特定的接口创建mock

这里假设我们有一个GreetingService的接口, 我们要为其创建mock

1
2
3

mockery --name GreetingService

我们可以使用 –name来指定我们需要生成mock的interface
由于我们没有指定查找GreetingService的目录, 所有我们要切换到与GreetingService同级的目录执行该命令.

3.4.2. 为多个接口生成mock

在项目中往往不只一个接口, 如果我们需要为多个接口生成mock应该怎么做呢? 下面即是使用mockery为多个接口生成mock的例子.
这里假设我们有两个接口GreetingService 和 OrderService 并且都处在项目根目录下.

1
2
3

mockery --name "GreetingService|OrderService"

同时我们也可以使用正则表达式来指导接口, 例如我们可以将上面的命令使用正则表达式简化一下, 因为它们的名字中都含有Service, 所以我们可以利用这个命名规范带来的便利. 正则表达式的语法不在本教程的讲解范围之内, 可以执行搜索相关主题了解.

1
2
3

mockery --name ".*Service"

甚至, 由于我们举的例子中只有两个接口, 在实际项目中我们也许会有这样的需求, 那就是为当前目录下所有的接口生成mock或更新mock. 那我们就可以这样做.

1
2
3

mockery --all

3.5. 指定查找service的路径

上面我们有一个假定, 多个接口都处在同一个目录, 而且都在根目录下. 这显然不符合项目实际, 在真实项目中, 往往接口是有层次结构并按类别分类存放的.

这里假设GreetingService在目录下的greeting目录下, 而OrderService在order目录下. 那么我们可以使用--dir选项来指定查找路径.

1
2
3

mockery --dir greeting --dir order --name "GreetingService|OrderService"

当然如果接口一多, 项目层次变深, 命令会变得很冗长, 这时我们可以使用-r--recursive在当前目录的所有子目录中递归查找接口, 例如

1
2
3

mockery -r --name "GreetingService|OrderService"

这样就可以很好的解决命令冗长琐碎的问题, 另外就我个人见解,实际上--recursive这种选项可以做成默认行为, 我不知道mockery为什么不这样做.

3.6. 为依赖包中的接口生成mock

有时我们的项目不仅仅需要mock 项目自身的接口, 有时也需要mock依赖包中的接口. 例如我们需要模拟sql.Result 这个接口.

此时我们可以使用--srcpkg这个选项.

1
mockery --srcpkg database/sql --name=Result

3.7. 修改输出目录

mockery默认的输出目录为项目根目录的mocks文件夹, 我们可以使用--output这个选项改变默认的output文件夹, 也可以使用--outpkg改变默认的包名

1
mockery -r --output mymock --name "GreetingService|OrderService"

改变默认package那么

1
mockery -r --output mymock --outpkg mymock --name "GreetingService|OrderService"

更多关于mockery使用, 可以使用mockery --help或查看官方文档.

3.8. mockery mock实战

这里依然以之前我的关于golang单元测试的中所使用的范例为例, 讲解使用mockery如何简化我们的测试.

3.9. 实现代码

我们创建一个非常简单的服务,如下所示:

GreetingService是一个向用户打招呼的服务。其由两种问候方式:
Greet()根据设置的语言向用户打招呼
GreetDefaultMessage()将使用默认消息向用户打招呼致意,不涉及到语言设置.
在GreetingService内部,Greet()将调用db.FetchMessage(lang),GreetDefaultMessage()将呼叫db.FetchDefaultMessage()。我们可以在真实场景想象的样子,db类是调用真实数据库的类。因此,我们需要在测试中使用mock来避免测试调用实际的数据库。golang中没有class的概念,但我们可以认为struct行为与类是等效的。

首先我们定义一个名为service包。然后,我们将创建一个dv结构及其接口,并将其命名为db。

DB.go

1
2
3
4
5
6
7
8
9
10
package service

type db struct{}

// DB is fake database interface.
type DB interface {
FetchMessage(lang string) (string, error)
FetchDefaultMessage() (string, error)
}

然后我们将创建GreetingService接口和实现一个调用DB接口的greeter struct。greeter struct构造函数第二个参数接收lang参数。

1
2
3
4
5
6
7
8
9
10
11
12

type greeter struct {
database DB
lang string
}

// GreetingService is service to greet your friends.
type GreetingService interface {
Greet() string
GreetInDefaultMsg() string
}

为了使数据库结构实现数据库接口,我们将添加所需的方法,并使用指针接收者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

func (d *db) FetchMessage(lang string) (string, error) {
// in real life, this code will call an external db
// but for this sample we will just return the hardcoded example value
if lang == "en" {
return "hello", nil
}
if lang == "es" {
return "holla", nil
}
return "bzzzz", nil
}

func (d *db) FetchDefaultMessage() (string, error) {
return "default message", nil
}

接下来,我们需要实现greeter的方法Greet()和GreetInDefaultMsg()。

1
2
3
4
5
6
7
8
9
10
11

func (g greeter) Greet() string {
msg, _ := g.database.FetchMessage(g.lang) // call database to get the message based on the lang
return "Message is: " + msg
}

func (g greeter) GreetInDefaultMsg() string {
msg, _ := g.database.FetchDefaultMessage() // call database to get the default message
return "Message is: " + msg
}

上面,greetiner方法将会调用DB以获取实际消息。
为Greeter和DB创建一个工厂方法用于创建greeter和db实例。

1
2
3
4
5
6
7
8

func NewDB() DB {
return new(db)
}

func NewGreeter(db DB, lang string) GreetingService {
return greeter{db, lang}
}

在实现的最后一部分,我们将编写一个主函数来运行服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"testify-mock/service"
)

func main() {
d := service.NewDB()

g := service.NewGreeter(d, "en")
fmt.Println(g.Greet()) // Message is: hello
fmt.Println(g.GreetInDefaultMsg()) // Message is: default message

g = service.NewGreeter(d, "es")
fmt.Println(g.Greet()) // Message is: holla

g = service.NewGreeter(d, "random")
fmt.Println(g.Greet()) // Message is: bzzzz
}

运行后的输出如下。

1
2
3
4
5
6
7

$ go run main.go
Message is: hello
Message is: default message
Message is: holla
Message is: bzzzz

3.10. Mock和测试

之前的博客中, 我们是手写Mock代码, 这次我们的Mock部分借助Mockery帮我们自动生成.

在生成Mock之前, 我们需要安装Mockery.

首先我们使用前面学到的知识为GreetingService生成mock

1
2
3

mockery -r --name "GreetingService|DB"

运行成功后, mockery帮我们生成了, 想要的mock如下

mocks/GreetingService.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
45
46
47
48
49
50
51
52
53
54

package mocks

import mock "github.com/stretchr/testify/mock"

// GreetingService is an autogenerated mock type for the GreetingService type
type GreetingService struct {
mock.Mock
}

// Greet provides a mock function with given fields:
func (_m *GreetingService) Greet() string {
ret := _m.Called()

var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}

return r0
}

// GreetInDefaultMsg provides a mock function with given fields:
func (_m *GreetingService) GreetInDefaultMsg() string {
ret := _m.Called()

var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}

return r0
}

type mockConstructorTestingTNewGreetingService interface {
mock.TestingT
Cleanup(func())
}

// NewGreetingService creates a new instance of GreetingService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewGreetingService(t mockConstructorTestingTNewGreetingService) *GreetingService {
mock := &GreetingService{}
mock.Mock.Test(t)

t.Cleanup(func() { mock.AssertExpectations(t) })

return mock
}


mocks/DB.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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75


package mocks

import mock "github.com/stretchr/testify/mock"

// DB is an autogenerated mock type for the DB type
type DB struct {
mock.Mock
}

// FetchDefaultMessage provides a mock function with given fields:
func (_m *DB) FetchDefaultMessage() (string, error) {
ret := _m.Called()

var r0 string
var r1 error
if rf, ok := ret.Get(0).(func() (string, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}

if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}

return r0, r1
}

// FetchMessage provides a mock function with given fields: lang
func (_m *DB) FetchMessage(lang string) (string, error) {
ret := _m.Called(lang)

var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
return rf(lang)
}
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(lang)
} else {
r0 = ret.Get(0).(string)
}

if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(lang)
} else {
r1 = ret.Error(1)
}

return r0, r1
}

type mockConstructorTestingTNewDB interface {
mock.TestingT
Cleanup(func())
}

// NewDB creates a new instance of DB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewDB(t mockConstructorTestingTNewDB) *DB {
mock := &DB{}
mock.Mock.Test(t)

t.Cleanup(func() { mock.AssertExpectations(t) })

return mock
}


4. Mock无参方法

在上一节中, 我们使用mockery cli创建了一个DB的mock struct, 现在我们可以在测试中使用它了.

在DB interface上有一个不带参数的方法FetchDefaultMessage, 我们想要在测试中模拟它. 我们可以像下面这样创建一个模拟对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package service_test

import (
"mocks"
"service"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMockMethodWithoutArgs(t *testing.T) {
theDBMock := &mocks.DB{} // create the mock
theDBMock.On("FetchDefaultMessage").Return("foofofofof", nil) // mock the expectation
g := service.NewGreeter(theDBMock, "en") // create greeter object using mocked db
assert.Equal(t, "Message is: foofofofof", g.GreetInDefaultMsg()) // assert what actual value that will come
theDBMock.AssertNumberOfCalls(t, "FetchDefaultMessage", 1) // we can assert how many times the mocked method will be called
theDBMock.AssertExpectations(t) // this method will ensure everything specified with On and Return was in fact called as expected
}

在上面的代码中, 我们创建了一个dbMock对象, 并使用On方法指定了要模拟的方法FetchDefaultMessage().
然后, 我们使用Return方法指定了模拟方法的返回值. 当该方法被调用时, 将返回我们指定的模拟值.

5. Mock带参数的方法

在上一节中, 我们已经了解了如何模拟没有参数的方法. 在这一节中, 我们将学习如何模拟带有参数的方法.

在DB interface上有一个带参数的方法FetchMessage(lang string), 我们想要在测试中模拟它. 我们可以像下面这样创建一个模拟对象:

1
2
3
4
5
6
7
8
9

func TestMockMethodWithArgs(t *testing.T) {
theDBMock := &mocks.DB{}
theDBMock.On("FetchMessage", "sg").Return("lah", nil) // if FetchMessage("sg") is called, then return "lah"
g := service.NewGreeter(theDBMock, "sg")
assert.Equal(t, "Message is: lah", g.Greet())
theDBMock.AssertExpectations(t)
}

6. 总结

在本文中我们介绍了mockery这个mock工具, 以及它的使用方法, 另外列出了两个mockery结合testify进行单元测试的实例, 希望对您有帮助.

7. 参考文档

使用testify和mockery库简化单元测试

作者

鹏叔

发布于

2023-04-27

更新于

2024-07-10

许可协议

评论