Golang单元测试gomock

1. go mock单元测试

搞单元测试,如果碰到这些情况:

  1. 一个函数,内部包含了很多并且很深的调用,但是如果单单测这个函数,其实实现的功能很简单。
  2. 一个函数,包含了其他还未实现的调用。
  3. 函数内部对数据的要求极为苛刻。

那么这时候就可以考虑使用mock来处理。

mock,简而言之就是可以通过注入我们所期望返回的数据,或者我们所期望传递的参数,来避免上面那些情况,其原理则是通过反射来实现。

2. go mock下载

gomock是go官方提供的mock解决方案,主要分为两部分:gomock库和mock代码生成工具mockgen。首先你需要做的是将依赖下载到本地:

go get github.com/golang/mock/gomock    #代码依赖
go get github.com/golang/mock/mockgen   #命令行工具

如何你设置过$GOPATH/bin到你的$PATH变量中,那么这里就可以直接运行mockgen命令了,否则需要使用绝对路径或者相当于$GOPATH的目录。

3. mock示例

└── mock1
├── metal_test.go
├── metal.go
└── mock_metal.go //通过mockgen自动生成

3.1. 示例代码metal.go

metal.go代码

//go: generate mockgen -package="mock1" -source="./metal.go" > mock_metal.go
package mock1

type Imetal interface {
    GetName() string
    SetName(string) string
}

func GetMetalName(mi Imetal) string {
    mi.GetName()
    return mi.GetName()
}

func SetMetalName(mi Imetal,name string) string {
     return  mi.SetName(name)
}

type Metal struct {
    Name string
    Exchange string
}

func (self Metal) GetName() string {
    if self.Name==""{
        return "none"
    }
    return self.Name
}

func (self *Metal) SetName(brand string) string {
    self.Name=brand
    return "done"
}

我现在有一个package,其包含了IMetal接口,这个接口下面有两个方法,现在针对这两个方法来进行mock,ps:gomock只支持interface方法的mock。

在mock之前,需要先通过mockgen来生成mock代码,我的源就是上面的IMeta接口。

3.2. 生成mock代码mock_metal.go

首先用gomock提供的mockgen工具生成要mock的接口的实现,这个工具是gomock提供的用来为要mock的接口生成实现的。它可以根据给定的接口,来自动生成代码。mockgen有两种工作模式:source和reflect

  • source模式
    根据源文件来生成,源文件是包含了一个或多个interface的文件。

    mockgen -source=foo.go [other options]
    #这里执行如下:
    mockgen -package="mock1" -source="./metal.go" > mock_metal.go
  • reflect模式
    一个文件定义了多个interface而你只想对部分interface进行mock,或者interface存在嵌套,使用reflect模式

    mockgen src/package Conn,Driver

如果编译报错read F:\Golang\src\test\mock1\mock_metal.go: unexpected NUL in input, 是因为windows下生成UTF-16字符文件,需要修改生成的字符为UTF-8格式,可以通过nodepad++进行修改转换格式;

mock代码生成好之后,接下来是写测试函数。

3.3. 测试函数metal_test.go

package mock1

import (
    "fmt"
    "testing"
    "github.com/golang/mock/gomock"
)

func TestMetalName(t *testing.T) {
    mockCtl := gomock.NewController(t)
    defer mockCtl.Finish()
    mockMetal := NewMockImetal(mockCtl) //NewMockImetal是生成的mock代码,以包的形式存在

    m := new(Metal)

    //获取metal name测试
    mockCtl.RecordCall(m, "GetName")
    mockCtl.Call(m, "GetName")

    mockMetal.EXPECT().GetName().Return("apple").MaxTimes(10) //call :=
    //mockMetal.EXPECT().GetName().Return("peer").After(call)          //注入期望的返回值

    mockedBrand := GetMetalName(mockMetal)

    //设置MetalName测试
    mockMetal.EXPECT().SetName(gomock.Eq("al")).Do(func(format string) { //入参校验
        fmt.Println("recv param :", format)
    }).Return("setdone")

    mockedSetName := SetMetalName(mockMetal, "al")
    fmt.Println(mockedSetName)

    if "peer" != mockedBrand {
        t.Error("Get wrong name:", mockedBrand)
    }

    if "setdone" != mockedSetName {
        t.Error("Set wrong name:", mockedSetName)
    }
}

然后执行go test即可,会发现这些被mock的函数会按照我们定义的行为来执行。结果如下:

recv param : al
setdone
--- FAIL: TestMetalName (0.00s)
    metal_test.go:39: Get wrong name: apple
FAIL
exit status 1
FAIL    test/mock1      0.297s

4. mockgen工具使用

在生成mock代码的时候,我们用到了mockgen工具,这个工具是gomock提供的用来为要mock的接口生成实现的。它可以根据给定的接口,来自动生成代码。这里给定接口有两种方式:接口文件和实现文件

4.1. 接口文件

如果有接口文件,则可以通过:

-source: 指定接口文件
-destination: 生成的文件名
-package:生成文件的包名
-imports: 依赖的需要import的包
-aux_files:接口文件不止一个文件时附加文件
-build_flags: 传递给build工具的参数

比如mock代码使用

mockgen -destination spider/mock_spider.go -package spider -source spider/spider.go

就是将接口spider/spider.go中的接口做实现并存在 spider/mock_spider.go文件中,文件的包名为"spider"。

4.2. 实现文件

在我们的上面的例子中,并没有使用"-source",那是如何实现接口的呢?mockgen还支持通过反射的方式来找到对应的接口。只要在所有选项的最后增加一个包名和里面对应的类型就可以了。其他参数和上面的公用。

4.3. 通过注释指定mockgen

如上所述,如果有多个文件,并且分散在不同的位置,那么我们要生成mock文件的时候,需要对每个文件执行多次mockgen命令(假设包名不相同)。这样在真正操作起来的时候非常繁琐,mockgen还提供了一种通过注释生成mock文件的方式,此时需要借助go的"go generate "工具。

在接口文件的注释里面增加如下:

//go:generate mockgen -destination mock_spider.go -package spider github.com/cz-it/blog/blog/Go/testing/gomock/example/spider Spider

这样,只要在spider目录下执行

go generate

命令就可以自动生成mock文件了。

5. gomock接口使用

在生成了mock实现代码之后,我们就可以进行正常使用了。这里假设结合testing进行使用(当然你也可考虑使用GoConvey)。我们就可以
在单元测试代码里面首先创建一个mock控制器:

mockCtl := gomock.NewController(t)

将* testing.T传递给gomock生成一个"Controller"对象,该对象控制了整个Mock的过程。在操作完后还需要进行回收,所以一般会在New后面defer一个Finish

defer mockCtl.Finish()

然后就是调用mock生成代码里面为我们实现的接口对象:

mockMetal := spider.NewmockMetal(mockCtl)

这里的"spider"是mockgen命令里面传递的报名,后面是NewMockXxxx格式的对象创建函数"Xxx"是接口名。这里需要传递控制器对象进去。返回一个接口的实现对象。

有了实现对象,我们就可以调用其断言方法了:EXPECT()

这里gomock非常牛的采用了链式调用法,和Swfit以及ObjectiveC里面的Masonry库一样,通过"."连接函数调用,可以像链条一样连接下去。

mockMetal.EXPECT().GetBody().Return("go1.8.3")

这里的每个"."调用都得到一个"Call"对象,该对象有如下方法:

func (c *Call) After(preReq *Call) *Call
func (c *Call) AnyTimes() *Call
func (c *Call) Do(f interface{}) *Call
func (c *Call) MaxTimes(n int) *Call
func (c *Call) MinTimes(n int) *Call
func (c *Call) Return(rets ...interface{}) *Call
func (c *Call) SetArg(n int, value interface{}) *Call
func (c *Call) String() string
func (c *Call) Times(n int) *Call

这里EXPECT()得到实现的对象,然后调用实现对象的接口方法,接口方法返回第一个"Call"对象,
然后对其进行条件约束。

上面约束都可以在文档中或者根据字面意思进行理解,这里列举几个例子:

5.1. 指定返回值

如我们的例子,调用Call的Return函数,可以指定接口的返回值:

mockMetal.EXPECT().GetBody().Return("go1.8.3")
这里我们指定返回接口函数GetBody()返回"go1.8.3"。

5.2. 指定执行次数

有时候我们需要指定函数执行多次,比如接受网络请求的函数,计算其执行了多少次。
mockMetal.EXPECT().Recv().Return(nil).Times(3)

执行三次Recv函数,这里还可以有另外几种限制:

AnyTimes() : 0到多次
MaxTimes(n int) :最多执行n次,如果没有设置
MinTimes(n int) :最少执行n次,如果没有设置

5.3. 指定执行顺序

有时候我们还要指定执行顺序,比如要先执行Init操作,然后才能执行Recv操作。

initCall := mockMetal.EXPECT().Init()
mockMetal.EXPECT().Recv().After(initCall)

6. 参考文章

发表评论

邮箱地址不会被公开。 必填项已用*标注