mockery v2的介绍和使用

guoapeng · · 5220 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

## 前言 由于项目时间比较紧, 我本来是没有打算写一篇文章来介绍mockery的, 但是无奈网上介绍mockery的文章比数量上较少(截至2023-04-27), 而且很多文章都过期了. 一方面由于golang更新比较快, 网上解释使用`go get` 安装mockery的, 到了go 1.6以后都安装不了. 另一方面mockery自身更新也比较快, 很多文章介绍的一些用法在新的版本中已经不灵了, 比如生成mock对象的命令选项`-name`已经调整为`--name`, `-dir`的意义也发生了变化等等, 出现了很多差异的地方. 所以本着稳扎稳打的原则, 不得不放慢脚步, 停下来把golang mock这一块的知识库补充完整. ## mockery介绍 Mockery是一个用于生成Golang接口的Mock的工具. Mockery可以帮助您在测试期间模拟依赖, 以便更轻松地测试代码. Mockery v2是Mocker的最新版本. <!-- more --> ### 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 v2还提供了一些新的命令, 例如mockery init, 它可以帮助我们在项目中设置Mockery. 执行mockery init命令将在当前目录中创建一个名为.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中引入的新特性泛型类型参数, 嵌入式结构体, 嵌入式接口和结构体的混合使用, 类型别名等等. ## 安装Mockery 安装mockery比较简单. 在Golang 1.16及以上的版本需要使用`go install` 安装prebuilt(也就是binary的程序)的Mockery工具, 如果使用的是golang 1.16以前的版本仍然使用`go get` 来安装. ### go install ```bash go install github.com/vektra/mockery/v2@v2.25.0 ``` 这里我安装的是mockery v2当前最新版本2.25.0版本, 版本信息可以在Mockery的[release notes](https://github.com/vektra/mockery/releases)页面找到 ### Docker Mockery也可以结合docker使用 下载docker image ```bash docker pull vektra/mockery ``` 使用Mockery生成Mock ```bash docker run -v "$PWD":/src -w /src vektra/mockery --all ``` ### Homebrew 在macOS上可以使用Hombrew来安装, 安装方法如下: ```bash brew install mockery brew upgrade mockery ``` ### Mockery CLI的使用 前面我们讲了Mockery是一个生成Mock的工具, 那么如何使用它呢, 这里就讲一讲Mockery CLI的用法. 讲解的过程中我们遵循由浅入深的规则. 先从简单的示例开始. #### 为某个特定的接口创建mock 这里假设我们有一个GreetingService的接口, 我们要为其创建mock ```bash mockery --name GreetingService ``` 我们可以使用 --name来指定我们需要生成mock的interface 由于我们没有指定查找GreetingService的目录, 所有我们要切换到与GreetingService同级的目录执行该命令. #### 为多个接口生成mock 在项目中往往不只一个接口, 如果我们需要为多个接口生成mock应该怎么做呢? 下面即是使用mockery为多个接口生成mock的例子. 这里假设我们有两个接口GreetingService 和 OrderService 并且都处在项目根目录下. ```bash mockery --name "GreetingService|OrderService" ``` 同时我们也可以使用正则表达式来指导接口, 例如我们可以将上面的命令使用正则表达式简化一下, 因为它们的名字中都含有Service, 所以我们可以利用这个命名规范带来的便利. 正则表达式的语法不在本教程的讲解范围之内, 可以执行搜索相关主题了解. ```bash mockery --name ".*Service" ``` 甚至, 由于我们举的例子中只有两个接口, 在实际项目中我们也许会有这样的需求, 那就是为当前目录下所有的接口生成mock或更新mock. 那我们就可以这样做. ```bash mockery --all ``` ### 指定查找service的路径 上面我们有一个假定, 多个接口都处在同一个目录, 而且都在根目录下. 这显然不符合项目实际, 在真实项目中, 往往接口是有层次结构并按类别分类存放的. 这里假设GreetingService在目录下的greeting目录下, 而OrderService在order目录下. 那么我们可以使用`--dir`选项来指定查找路径. ```bash mockery --dir greeting --dir order --name "GreetingService|OrderService" ``` 当然如果接口一多, 项目层次变深, 命令会变得很冗长, 这时我们可以使用`-r`或`--recursive`在当前目录的所有子目录中递归查找接口, 例如 ```bash mockery -r --name "GreetingService|OrderService" ``` 这样就可以很好的解决命令冗长琐碎的问题, 另外就我个人见解,实际上`--recursive`这种选项可以做成默认行为, 我不知道mockery为什么不这样做. ### 为依赖包中的接口生成mock 有时我们的项目不仅仅需要mock 项目自身的接口, 有时也需要mock依赖包中的接口. 例如我们需要模拟sql.Result 这个接口. 此时我们可以使用`--srcpkg`这个选项. ```bash mockery --srcpkg database/sql --name=Result ``` ### 修改输出目录 mockery默认的输出目录为项目根目录的mocks文件夹, 我们可以使用`--output`这个选项改变默认的output文件夹, 也可以使用`--outpkg`改变默认的包名 ```bash mockery -r --output mymock --name "GreetingService|OrderService" ``` 改变默认package那么 ```bash mockery -r --output mymock --outpkg mymock --name "GreetingService|OrderService" ``` 更多关于mockery使用, 可以使用`mockery --help`或查看官方文档. ### mockery mock实战 这里依然以之前我的关于golang单元测试的中所使用的范例为例, 讲解使用mockery如何简化我们的测试. ### 实现代码 我们创建一个非常简单的服务,如下所示: GreetingService是一个向用户打招呼的服务。其由两种问候方式: Greet()根据设置的语言向用户打招呼 GreetDefaultMessage()将使用默认消息向用户打招呼致意,不涉及到语言设置. 在GreetingService内部,Greet()将调用db.FetchMessage(lang),GreetDefaultMessage()将呼叫db.FetchDefaultMessage()。我们可以在真实场景想象的样子,db类是调用真实数据库的类。因此,我们需要在测试中使用mock来避免测试调用实际的数据库。golang中没有class的概念,但我们可以认为struct行为与类是等效的。 首先我们定义一个名为service包。然后,我们将创建一个dv结构及其接口,并将其命名为db。 DB.go ```golang 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参数。 ```golang type greeter struct { database DB lang string } // GreetingService is service to greet your friends. type GreetingService interface { Greet() string GreetInDefaultMsg() string } ``` 为了使数据库结构实现数据库接口,我们将添加所需的方法,并使用指针接收者。 ```golang 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()。 ```golang 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实例。 ```golang func NewDB() DB { return new(db) } func NewGreeter(db DB, lang string) GreetingService { return greeter{db, lang} } ``` 在实现的最后一部分,我们将编写一个主函数来运行服务。 ```golang 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 } ``` 运行后的输出如下。 ```bash $ go run main.go Message is: hello Message is: default message Message is: holla Message is: bzzzz ``` ### Mock和测试 之前的博客中, 我们是手写Mock代码, 这次我们的Mock部分借助Mockery帮我们自动生成. 在生成Mock之前, 我们需要安装Mockery. 首先我们使用前面学到的知识为GreetingService生成mock ```bash mockery -r --name "GreetingService|DB" ``` 运行成功后, mockery帮我们生成了, 想要的mock如下 mocks/GreetingService.go ```golang 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 ```golang 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 } ``` ## Mock无参方法 在上一节中, 我们使用mockery cli创建了一个DB的mock struct, 现在我们可以在测试中使用它了. 在DB interface上有一个不带参数的方法FetchDefaultMessage, 我们想要在测试中模拟它. 我们可以像下面这样创建一个模拟对象: ```golang 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), 我们想要在测试中模拟它. 我们可以像下面这样创建一个模拟对象: ```golang 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) } ``` ## 总结 在本文中我们介绍了mockery这个mock工具, 以及它的使用方法, 另外列出了两个mockery结合testify进行单元测试的实例, 希望对您有帮助. ## 参考文档 [使用testify和mockery库简化单元测试](https://studygolang.com/articles/15992)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

5220 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传