专栏首页技术之路go test 测试用例那些事(二) mock

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目录daoservice下的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传入的文件,把提取文件内所有的importinterface,然后遍历所有的接口方法,判断参数属于哪个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等。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 剖析nsq消息队列(四) 消息的负载处理

    实际应用中,一部分服务集群可能会同时订阅同一个topic,并且处于同一个channel下。当nsqd有消息需要发送给订阅客户端去处理时,发给哪个客户端是需要考...

    lpxxn
  • Go 单例模式[个人翻译]

      原文地址:http://marcio.io/2015/07/singleton-pattern-in-go/   最近几年go语言的增长速度非常惊人,吸引...

    lpxxn
  • c++ 头文件

    可以将程序分为二部分: 头文件:包含结构声明和使用这些结构的函数的原型 源代码文件: 包含与结构有关的函数的代码 不要将函数的定义或变量的声明放在头文件里, 一...

    lpxxn
  • 消除IE stop running this script弹出框

    A script on thispage is causing your web browser to run slowly. If it continues ...

    大菊观
  • 协助你写 Python,只是 AI 取代程序员的第一步

    按照 AI 现在发展的态势,应该是计划先取代送货员,再取代驾驶员,接着取代前台、保安、售票员等等。最后总有一天会把魔爪伸向一手打造他们的工程师,不过在这一天来临...

    HyperAI超神经
  • 主从复制的原理

    宇宙之一粟
  • 都说Linux很重要,你会几个Linux命令?来看看这道面试题目。

    今天继续讲解美团java一面面试题目,Linux怎么搜索文件中的字符串,并把这个字符串所在行和下面一行的内容写到另外一个文件中?多了解Linux操作系统基本的命...

    用户7656790
  • ASTMatcher分析函数调用链(下)

    上一篇文章(ASTMatcher分析函数调用链(上))讲到ASTMatcher的原理以及创建,本文将详细介绍ASTMatcher获取函数调用链在iOS app中...

    adding
  • laravel通用化的CURD的实现

    composer require shencongcong/laravel-curd ~1.0

    砸漏
  • 读写二进制文件与文本文件

    文件分为两类:二进制文件和文本文件。所有数据在计算机中均以二进制形式存在,这里所说的二进制和文本是以程序解释文件数据的方式来区分的。

    雪飞鸿

扫码关注云+社区

领取腾讯云代金券