菜鸟笔记
提升您的技术认知

go mock 学习总结-ag真人游戏

go mock 学习总结

gomock是由golang官方开发维护的测试框架,实现了较为完整的基于interface的mock功能,能够与golang内置的testing包良好集成,也能用于其它的测试环境中。gomock测试框架包含了gomock包和mockgen工具两部分,其中gomock包完成对桩对象生命周期的管理,mockgen工具用来生成interface对应的mock类源文件。

1.安装与部署

  • 下载源文件

      go get github.com/golang/mock/gomock
    

    运行完后你会发现,在$gopath/src目录下有了github.com/golang/mock子目录,且在该子目录下有gomock包和mockgen工具。然后继续执行以下命令

  • 编译

      cd $gopath/src/github.com/golang/mock/mockgen
      go build
    

    在当前目录下生成了一个可执行程序mockgen。然后将mockgen程序移动到$gopath/bin目录下:

      mv mockgen $gopath/bin
    

这时在命令行运行mockgen,如果列出了mockgen的使用方法和例子,则说明mockgen已经安装成功,

  1. 使用说明
  • 官方文档

2. 使用方法

1.定义接口文件

go mock 的主要功能就是针对接口进行mock ,因此 第一步首先应该构建接口文件供测试使用。

假设接口定义如下:

infra/foo.go

package db
type repository interface {
    create(key string, value []byte) error
    retrieve(key string) ([]byte, error)
    update(key string, value []byte) error
    delete(key string) error
}

2.生成mock类文件

mockgen 针对interface的mock有两种模式,分别是源文件模式和反射

  • 源文件模式:利用mockgen命令,便可以针对一个包含interface的源文件生成一个mock类的源文件。mockgen支持的选项如下:

    • -source: 一个文件包含打算mock的接口列表
    • -destination: 存放mock类代码的文件。如果你没有设置这个选项,代码将被打印到标准输出
    • -package: 用于指定mock类源文件的包名。如果你没有设置这个选项,则包名由mock_和输入文件的包名级联而成
    • -aux_files: 参看附加的文件列表是为了解析类似嵌套的定义在不同文件中的interface。指定元素列表以逗号分隔,元素形式为foo=bar/baz.go,其中bar/baz.go是源文件,foo是-source选项指定的源文件用到的包名

具体命令如下

mockgen -source=foo.go [other options]

注意:第一个参数是基于gopath的相对路径,第二个参数可以为多个interface,并且interface之间只能用逗号分隔,不能有空格。

  • 反射:一个文件定义了多个interface而你只想对部分interface进行mock,或者interface存在嵌套时,便需要使用反射模式。由于 -destination 选项输入太长,因此使用重定向符号 >,并且mock类代码的输出文件的路径必须是绝对路径

具体命令如下:

mockgen infra(文件名)/db(包名) repository(interfacename) > $gopath/src/test/mock/db/mock_repository.go

注意

  • 输出目录test/mock/db必须提前建好,否则mockgen会运行失败
  • 若工程中的第三方库统一放在vendor目录下,则需要拷贝一份gomock的代码到$gopath/src下,gomock的代码即github.com/golang/mock/gomock,这是因为mockgen命令运行时要在这个路径访问gomock

可以在test/mock/db目录下看到mock_repository.go文件已经生成,该文件的代码片段如下:

// automatically generated by mockgen. do not edit!
// source: infra/db (interfaces: repository)
package mock_db
import (
    gomock "github.com/golang/mock/gomock"
)
// mockrepository is a mock of repository interface
type mockrepository struct {
    ctrl     *gomock.controller
    recorder *mockrepositorymockrecorder
}
// mockrepositorymockrecorder is the mock recorder for mockrepository
type mockrepositorymockrecorder struct {
    mock *mockrepository
}
// newmockrepository creates a new mock instance
func newmockrepository(ctrl *gomock.controller) *mockrepository {
    mock := &mockrepository{ctrl: ctrl}
    mock.recorder = &mockrepositorymockrecorder{mock}
    return mock
}
// expect returns an object that allows the caller to indicate expected use
func (_m *mockrepository) expect() *mockrepositorymockrecorder {
    return _m.recorder
}
// create mocks base method
func (_m *mockrepository) create(_param0 string, _param1 []byte) error {
    ret := _m.ctrl.call(_m, "create", _param0, _param1)
    ret0, _ := ret[0].(error)
    return ret0
}

3.使用mock对象进行测试

1. 创建mock 控制器

mock控制器通过newcontroller接口生成,是mock生态系统的顶层控制,它定义了mock对象的作用域和生命周期,以及它们的期望。多个协程同时调用控制器的方法是安全的。当用例结束后,控制器会检查所有剩余期望的调用是否满足条件。

ctrl := newcontroller(t)
defer ctrl.finish()
2. 创建mock对象

mock对象创建时需要注入控制器,如果有多个mock对象则注入同一个控制器,如下所示:

ctrl := newcontroller(t)
defer ctrl.finish()
mockrepo := mock_db.newmockrepository(ctrl)
mockhttp := mock_api.newhttpmethod(ctrl)
3. mock对象的行为注入

对于mock对象的行为注入,控制器是通过map来维护的,一个方法对应map的一项。因为一个方法在一个用例中可能调用多次,所以map的值类型是数组切片。当mock对象进行行为注入时,控制器会将行为add。当该方法被调用时,控制器会将该行为remove。

对于go mock来说 ,他最强大的地方在于可以构造出我们想要要的结果供我们提供测试,
假设有这样一个场景:先retrieve领域对象失败,然后create领域对象成功,再次retrieve领域对象就能成功。这个场景对应的mock对象的行为注入代码如下所示:

mockrepo.expect().retrieve(any()).return(nil, errany)
mockrepo.expect().create(any(), any()).return(nil)
mockrepo.expect().retrieve(any()).return(objbytes, nil)

objbytes是领域对象的序列化结果,比如:

obj := movie{...}
objbytes, err := json.marshal(obj)
...	

当批量create对象时,可以使用times关键字:

mockrepo.expect().create(any(), any()).return(nil).times(5)

当批量retrieve对象时,需要注入多次mock行为:

mockrepo.expect().retrieve(any()).return(objbytes1, nil)
mockrepo.expect().retrieve(any()).return(objbytes2, nil)
mockrepo.expect().retrieve(any()).return(objbytes3, nil)
mockrepo.expect().retrieve(any()).return(objbytes4, nil)
mockrepo.expect().retrieve(any()).return(objbytes5, nil)
4. 确保行为调用的顺序

默认情况下,行为调用顺序可以和mock对象行为注入顺序不一致,即不保序。如果要保序,有两种方法:

 1. 通过after关键字来实现保序
 2. 通过inorder关键字来实现保序
  • 通过after保序

      retrievecall := mockrepo.expect().retrieve(any()).return(nil, errany)
      createcall := mockrepo.expect().create(any(), any()).return(nil).after(retrievecall)
      mockrepo.expect().retrieve(any()).return(objbytes, nil).after(createcall)
    
  • 通过inorder关键字实现保序

      inorder(
          mockrepo.expect().retrieve(any()).return(nil, errany)
          mockrepo.expect().create(any(), any()).return(nil)
          mockrepo.expect().retrieve(any()).return(objbytes, nil)
      )	
    
  • inorder关键字实现的保序更简单自然,所以推荐这种方式。

  • 当mock对象行为的注入保序后,如果行为调用的顺序和其不一致,就会触发测试失败。这就是说,对于上面的例子,如果在测试用例执行过程中,repository的方法的调用顺序如果不是按 retrieve -> create -> retrieve 的顺序进行,则会导致测试失败。

5. 使用mock对象

mock对象的行为都注入到控制器以后,我们接着要将mock对象注入给interface,使得mock对象在测试中生效。常见的方法是直接进行set.这种方法有一个缺陷:当测试用例执行完成后,并没有回滚interface到真实对象,有可能会影响其它测试用例的执行。在这里推荐使用gostub框架。

关于gostub框架//todo

总结:

  1. 编写测试用例有一些基本原则,我们一起回顾一下:
  2. 每个测试用例只关注一个问题,不要写大而全的测试用例
  3. 测试用例是黑盒的
  4. 测试用例之间彼此独立,每个用例要保证自己的前置和后置完备
  5. 测试用例要对产品代码非入侵
网站地图