在Golang中使用Testify mock框架

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

- [1. 前言](#1-前言) - [2. 实现代码](#2-实现代码) - [3. Mock和测试](#3-mock和测试) - [4. Mock无参方法](#4-mock无参方法) - [5. Mock带参数的方法](#5-mock带参数的方法) - [6. Mock带参数的方法, 但是参数具体内容非测试重点](#6-mock带参数的方法-但是参数具体内容非测试重点) - [7. Mock带参数的方法, 并校验实际参数](#7-mock带参数的方法-并校验实际参数) - [8. Mockery](#8-mockery) - [9. 参考](#9-参考) ## 1. 前言 我使用golang已经有一段时间了,但直到最近我才终于明白如何在golang测试中进行对象mocking。由于我来自Java,所以在golang中mock对象的方式对我来说并不清楚。这篇文章是我如何达到目前理解的自我记录。在这篇文章中,我使用了来自Testify的mock功能 在阅读这篇这篇文章之前, 读者需要有golang基础。 首先, 我们来创建一个非常简单的服务,如下所示: GreeterService是一个向用户打招呼的服务。其由两种问候方式: Greet()根据设置的语言向用户打招呼 GreetDefaultMessage()将使用默认消息向用户打招呼致意,不涉及到语言设置. 在GreeterService内部,Greet()将调用db.FetchMessage(lang),GreetDefaultMessage()将呼叫db.FetchDefaultMessage()。我们可以在真实场景想象的样子,db类是调用真实数据库的类。因此,我们需要在测试中使用mock来避免测试调用实际的数据库。golang中没有class的概念,但我们可以认为struct行为与类是等效的。 ## 2. 实现代码 首先我们定义一个名为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) } ``` 然后我们将创建GreeterService接口和实现一个调用DB接口的greeter struct。greeter struct构造函数第二个参数接收lang参数。 ```golang type greeter struct { database DB lang string } // GreeterService is service to greet your friends. type GreeterService 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) GreeterService { 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 ``` ## 3. Mock和测试 在上面的实现完成后, 我们将编写一个测试并模拟DB行为. 如前所述,我们希望在运行测试时防止调用实际数据库. 为了实现这个目标, 我们将mock DB接口. 不幸的是, 在golang中创建模拟对象并不像在Java中那样直截了当. 在Java中使用mockito, mocking可以像下面这样这么简单: ```java GreetingService mock = Mockito.mock(GreetingService.class); ``` 但是在golang中, 我们需要创建一个新的结构体并将testify模拟对象嵌入其中, 如下所示: ```golabg type dbMock struct { mock.Mock } ``` 然后, 为了使该模拟对象符合DB接口, 我们还需要手动实现接口的所有方法. 还有一个指定方法我们需要去调用它. 第一: 如果被模拟的方法有参数的话, 我们需要调用Mock.Called(args)接收参数 第二: 调用的放回值将用作我们要模拟的方法的返回值. 两种方法都返回(string, error). 因此, 模拟方法的返回语句是return args.String(0), args.Error(1). 返回语句的规则是`args.<ReturnValueType>(<index>)`. 索引从零开始 ```golang func (d *dbMock) FetchMessage(lang string) (string, error) { args := d.Called(lang) return args.String(0), args.Error(1) } func (d *dbMock) FetchDefaultMessage() (string, error) { args := d.Called() return args.String(0), args.Error(1) } ``` 按照这个规则, 假设我们要模拟的方法的返回值类型为(int, string, bool), 那么我们在mock方法返回值时需要这样写 return args.Int(0), args.String(1), args.Bool(2) 如果返回值类型中有复杂类型, 如结构体, 接口之类的, 那么return语句应该像这样写. ```golang return args.Get(0).(*MyObject), args.Get(1).(*AnotherObjectOfMine) ``` 至此我们几乎创建了mockito一句完成的所有内容, 注意这里的用词是几乎, 这里只是创建了struct, 再创建一个实例对象, 就完全完成了mockito帮助我们完成的所有工作. 这些mock步骤都是机械式的, 好在golang帮我实现了一些自动化工具, 这些mock代码也不用我们自己动手写. 手写的过程对于我们理解mock的底层原理还是有帮助的. 有关go mock更多额外信息,请参阅[testift go doc](https://godoc.org/github.com/stretchr/testify/mock)。 ## 4. Mock无参方法 在上一节中, 我们创建了一个DB的mock struct, 现在我们可以在测试中使用它了.在这一节中, 我们将了解如何使用Testify模拟不带参数的方法. 在DB interface上有一个不带参数的方法FetchDefaultMessage, 我们想要在测试中模拟它. 我们可以像下面这样创建一个模拟对象: ```golang func TestMockMethodWithoutArgs(t *testing.T) { theDBMock := dbMock{} // create the mock theDBMock.On("FetchDefaultMessage").Return("foofofofof", nil) // mock the expectation g := greeter{&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 := dbMock{} theDBMock.On("FetchMessage", "sg").Return("lah", nil) // if FetchMessage("sg") is called, then return "lah" g := greeter{&theDBMock, "sg"} assert.Equal(t, "Message is: lah", g.Greet()) theDBMock.AssertExpectations(t) } ``` 在上面的代码中, 我们创建了一个dbMock对象, 并使用On方法指定了要模拟的方法FetchMessage和参数"sg". 然后我们使用Return方法指定了模拟方法的返回值. 现在当我们在测试中调用FetchMessage("sg")方法时, 将返回我们指定的模拟值. 如果我们想要校验实际参数是否与预期相同, 我们可以使用AssertExpectations方法. ## 6. Mock带参数的方法, 但是参数具体内容非测试重点 有时我们想模拟一个方法,但我们不在乎传递的实际参数。为此,我们可以在On()方法参数后面的第二个参数中使用mock.Anything。 ```golang func TestMockMethodWithArgsIgnoreArgs(t *testing.T) { theDBMock := dbMock{} theDBMock.On("FetchMessage", mock.Anything).Return("lah", nil) // if FetchMessage(...) is called with any argument, please also return lah g := greeter{&theDBMock, "in"} assert.Equal(t, "Message is: lah", g.Greet()) theDBMock.AssertCalled(t, "FetchMessage", "in") theDBMock.AssertNotCalled(t, "FetchMessage", "ch") theDBMock.AssertExpectations(t) mock.AssertExpectationsForObjects(t, &theDBMock) } ``` ## 7. Mock带参数的方法, 并校验实际参数 有时我们需要模拟一个具有复杂参数的方法,但希望根据参数的某些属性或从中进行计算来匹配mock。例如,我们想模仿FetchMessage方法,但前提是lang参数以字母i开头。 ```golang func TestMatchedBy(t *testing.T) { theDBMock := dbMock{} theDBMock.On("FetchMessage", mock.MatchedBy(func(lang string) bool { return lang[0] == 'i' })).Return("bzzzz", nil) // all of these call FetchMessage("iii"), FetchMessage("i"), FetchMessage("in") will match g := greeter{&theDBMock, "izz"} msg := g.Greet() assert.Equal(t, "Message is: bzzzz", msg) theDBMock.AssertExpectations(t) } ``` ## 8. Mockery 正如我们在上一节中所看到的,在进行实际测试和模拟行为之前,我们需要手动创建mock结构体。[Mockery](https://github.com/vektra/mockery)可以帮助我们摆脱手工劳动。 首先,我们只需要安装Mockery: ```bash go get github.com/vektra/mockery/.../ ``` 然后生成mock ```bash mockery -name <interfaceToMock> ``` 将生成一个包含现成mock的文件,我们可以将其添加到测试中。 本文中示例代码的完整来源可在此[gitlab仓库](https://github.com/lamida/testify-mock)中获得。 ## 9. 参考 [Mocking in Golang Using Testify](https://blog.lamida.org/mocking-in-golang-using-testify/)

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

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

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