go test 测试用例那些事(二) mock
关于go
的单元测试,之前有写过一篇帖子go test测试用例那些事,但是没有说go官方的库mock,很有必要单独说一下这个库,和他的实现原理。
mock
主要的功能是对接口的模拟,需要在写代码的时候定义抽象很多接口,有时为了能方便go test
可能会多写一些冗余代码,但这些工作会让你的单元测试更灵活。特别是逻辑比较复杂的时候,上层要调用其他层的方法进行单元测试,会让单元测试越写越麻烦,越写越复杂,这也是很多人不喜欢写单元测试的原因。使用mock
模拟底层的接口,能让你只关注上层需要测试的逻辑,而不用为了测试一个功能,写一堆调用的底层的相关的测试逻辑。
使用
mockgen
就是mock的可执行命令。使用也很简单
mockgen -source=src.go [other options]
比如我们有一个接口
package d1
type User interface {
Name() string
SetAge(age int) bool
V(idx int, name string) (string, error)
}
执行mockgen
命令
mockgen -source=user.go
这里只指写了-source
会直接在控制台输出。也可以指定输出目录和输出包名称
mockgen -source=user.go -destination ./dao/u_mock.go -package mock_data
或者使用 go generate
来生成,需要在包名字上面加上下面这句。
//go:generate mockgen -destination ./dao/u_mock.go -package mock_data -source user.go
然后执行go generate ./...
和上面是一样的效果。
虽然go generate
很方便,但如果目标文件或者包名字有变动里,就需要修改所有文件。不如用命令来的快,直接写一个Makefile
进行指处理,下面是一个小例子,实现mock
目录dao
和service
下的go
文件,去掉了*_test.go
和一些指定的文件。
DAO_DIR=./dao
DAO_MOCK_DIR=$(DAO_DIR)/mock_dao
DAO_FILES=$(shell find $(DAO_DIR) -not -path "$(DAO_MOCK_DIR)/*" -type f -name "*.go" -not -name "*_test.go" -not -name "dao_init.go" -not -name "dao.go")
SERVICE_DIR=./service
SERVICE_MOCK_DIR=$(SERVICE_DIR)/mock_srv
SERVICE_FILES=$(shell find $(SERVICE_DIR) -not -path "$(SERVICE_MOCK_DIR)/*" -type f -name "*.go" -not -name "*_test.go" -not -name "service.go" -not -name "system_filter.go")
define gen-mock-file
@for f in $(3); do
eval t=`echo $$f | sed 's#$(1)#$(2)#'` ;
mockgen -source=$$f -destination=$$t ;
done
endef
.PHONY: gen-mock-dao
gen-mock-dao:
$(call gen-mock-file,$(DAO_DIR),$(DAO_MOCK_DIR),$(DAO_FILES))
.PHONY: gen-mock-service
gen-mock-service:
$(call gen-mock-file,$(SERVICE_DIR),$(SERVICE_MOCK_DIR),$(SERVICE_FILES))
gen-mock-all:
@echo begin gen code
@$(MAKE) gen-mock-dao
@$(MAKE) gen-mock-service
@echo done
使用
使用也很简单直接调用EXPECT()
然后给具体的方法指定参数,参数可以是任意的如下面的V
方法的第一个参数gomock.Any()
,参数可以是具体的值比如下面的2
,然后调用Return
指写返回指定的值。最后指定这个方法调用多少次,下面是调用的AnyTimes()
,当然也可以调用MinTimes
或者MaxTimes
指定次数
func TestUser1(t *testing.T) {
mockUser := mock_data.NewMockUser(gomock.NewController(t))
mockUser.EXPECT().V(gomock.Any(), "2").Return("a", nil).AnyTimes()
var u User = mockUser
a, err := u.V(1, "2")
t.Log(a, err)
}
Return
如果不调用会返回参数的默认值,上面的方法不如果不调用Return
会返回 "", nil
。
对于简单的逻辑可以直接调用Return
方法,返回指定的结果。但实际情况可能需要进行一些逻辑处理,返回动态的数据,可能通过DoAndReturn
mockUser := mock_data.NewMockUser(gomock.NewController(t))
mockUser.EXPECT().V(1, "2").DoAndReturn(func(idx int, n string) (string, error) {
t.Log(idx, " ", n)
return "1", nil
})
可以有多个DoAndReturn
,但只有最后一个的 return
会生效。
如果只想对传入的参数进行逻辑处理,可以调用Do
方法。
mockUser.EXPECT().V(1, "2").Do(func(id int, name string) {
t.Log(id, " ", name)
}).Do(func(id int, name string) {
t.Log("do2 ", id)
}).Return("a", nil)
当然根据自己的需要可以有多个Do
方法的处理。
mock
实现原理
实现的原理是根据go
强大的抽象语法树
实现的,说一个题外话除了mock库,还有一个依赖注入的库wire也是依赖抽象语法树实现的。
抽象语法树分析-source
传入的文件,把提取文件内所有的import
和interface
,然后遍历所有的接口方法,判断参数属于哪个import
,组织成结构,生成模拟结构实现提取的接口。
看一下生成的两个struct
// MockUser is a mock of User interface
type MockUser struct {
ctrl *gomock.Controller
recorder *MockUserMockRecorder
}
// MockUserMockRecorder is the mock recorder for MockUser
type MockUserMockRecorder struct {
mock *MockUser
}
上面的MockUser
具体实现了我们的接口User
。下面的MockUserMockRecorder
才是重头戏,保存着我们传入的的指定参数传Do
方法Return
方法等。
// NewMockUser creates a new mock instance
func NewMockUser(ctrl *gomock.Controller) *MockUser {
mock := &MockUser{ctrl: ctrl}
mock.recorder = &MockUserMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUser) EXPECT() *MockUserMockRecorder {
return m.recorder
}
EXPECT()
方法返回的就是MockUserMockRecorder
看一下我们的例子方法V
// V mocks base method
func (m *MockUser) V(idx int, name string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "V", idx, name)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// V indicates an expected call of V
func (mr *MockUserMockRecorder) V(idx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "V", reflect.TypeOf((*MockUser)(nil).V), idx, name)
}
返回的*gomock.Call
就是最底层的数据结构,保存的所有的自定义参数
type Call struct {
t TestHelper // for triggering test failures on invalid call setup
receiver interface{} // the receiver of the method call
method string // the name of the method
methodType reflect.Type // the type of the method
args []Matcher // the args
origin string // file and line number of call setup
preReqs []*Call // prerequisite calls
// Expectations
minCalls, maxCalls int
numCalls int // actual number made
// actions are called when this Call is called. Each action gets the args and
// can set the return values by returning a non-nil slice. Actions run in the
// order they are created.
actions []func([]interface{}) []interface{}
}
-
method``methodType
保存的方法的信息,mock
是从反射字段methodType
知道传入参数和返回结果的信息。 -
args
用于保存指定的参数, 是gomock.Any()
还是gomock.Eq()
等,进行传入参数匹配。 -
minCalls maxCalls
用于保存调用次数的限制 -
actions
用于保存我们的方法自定义方法Do
Return
DoReturn
等。
- JQuery笔记(三) jquery的用途
- Heartbeat使用梳理
- JQuery笔记(二) animate支持的属性
- 腾讯叮当首次系统性展示AI能力 开放日展示实战型解决方案
- Execute 方法(Find 对象)
- 在容器中部署mysql与数据持久化
- silverlight如何在运行时用代码动态控制(或创建)动画
- 小程序深夜连发4大功能!你们要的直播来了!
- 机器学习在现实生活中到底有哪些应用?
- 千锋郑州告诉你未来十年Python市场前景如何
- 读书笔记:基于web的工作流引擎设计
- 地图知识-坐标网
- silverlight/xap如何接收参数?
- Silverlight中多个Xaml("场景"? or "窗口"? )之间的切换/调用/弹出/传参数问题小结
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Android实现截屏方式整理(总结)
- 用RSHINY DASHBOARD可视化美国投票记录
- Android 侧滑关闭Activity的实例
- Android 两个Fragment之间的跳转和数据的传递实例详解
- Android编程实现保存图片到系统图库的方法示例
- Android自定义单选多选下拉列表的实例代码
- R语言POT超阈值模型在洪水风险频率分析中的应用研究
- Android开发之ToggleButton实现开关效果示例
- Android使用Circular Reveal动画让页面跳转更炫酷
- 学习使用Material Design控件(三)使用CardView实现卡片效果
- Android实现dialog的3D翻转示例
- Android ImageView 固定宽高比例的实现方法
- Android 实现IOS选择拍照相册底部弹出的实例
- ubuntu 20.04上搭建LNMP环境的方法步骤
- Android实现界面内嵌多种卡片视图(ViewPager、RadioGroup)