关于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
可以有多个DoAndReturn,但只有最后一个的 return会生效。
如果只想对传入的参数进行逻辑处理,可以调用Do方法。
当然根据自己的需要可以有多个Do方法的处理。
mock实现原理实现的原理是根据go强大的抽象语法树实现的,说一个题外话除了mock库,还有一个依赖注入的库wire也是依赖抽象语法树实现的。
抽象语法树分析-source传入的文件,把提取文件内所有的import和interface,然后遍历所有的接口方法,判断参数属于哪个import,组织成结构,生成模拟结构实现提取的接口。
看一下生成的两个struct
上面的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 }