gomock 源码分析
Posted golang算法架构leetcode技术php
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gomock 源码分析相关的知识,希望对你有一定的参考价值。
在分析完gostub的源码实现后https://mp.weixin.qq.com/s/I6urCBHbcfZCNaWw1iZmnA,我们分析下gomock的源码实现。两者原理相似,但是也有不同。
1,gomock的用法
反射模式
通过构建一个程序用反射理解接口生成一个mock类文件,它通过两个非标志参数生效:导入路径和用逗号分隔的符号列表(多个interface)。
mockgen -destination mock_sql_driver.go database/sql/driver Conn,Driver
源码模式
通过一个包含interface定义的文件生成mock类文件,它通过 -source 标识生效,-imports 和 -aux_files 标识在这种模式下也是有用的。
mockgen -source=exp1/foo.go -destination=exp1/mock/mock_foo.go
mock控制器
mock控制器通过NewController接口生成,是mock生态系统的顶层控制,它定义了mock对象的作用域和生命周期,以及它们的期望。多个协程同时调用控制器的方法是安全的。
当用例结束后,控制器会检查所有剩余期望的调用是否满足条件。
mock对象的行为注入
对于mock对象的行为注入,控制器是通过map来维护的,一个方法对应map的一项。因为一个方法在一个用例中可能调用多次,所以map的值类型是数组切片。当mock对象进行行为注入时,控制器会将行为Add。当该方法被调用时,控制器会将该行为Remove。
行为调用的保序
默认情况下,行为调用顺序可以和mock对象行为注入顺序不一致,即不保序。如果要保序,有两种方法:
通过After关键字来实现保序
通过InOrder关键字来实现保序
关键字InOrder是After的语法糖,源码如下:
// InOrder declares that the given calls should occur in order.
func InOrder(calls ...*Call) {
for i := 1; i < len(calls); i++ {
calls[i].After(calls[i-1])
}
}
具体使用一个例子
package foo
import (
"testing"
"time"
"github.com/golang/mock/gomock"
mock_foo "gomock_study/exp1/mock"
)
func TestSUT(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
m := mock_foo.NewMockFoo(ctl)
bar := m.
EXPECT().
Bar(gomock.Eq(99)).
DoAndReturn(func(_ int) int {
time.Sleep(1 * time.Second)
return 101
}).
AnyTimes()
m.EXPECT().Bar1(gomock.Any()).After(bar)
// Does not make any assertions. Returns 103 when Bar is invoked with 101.
m.
EXPECT().
Bar(gomock.Eq(101)).
Return(103).
AnyTimes()
SUT(m)
type args struct {
f Foo
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
{
name: "case1",
args: args{
f: m,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
SUT(tt.args.f)
})
}
}
2,gomock源码分析
比如上面的例子,接口定义如下
package foo
import "fmt"
type Foo interface {
Bar(x int) int
Bar1(x int) int
}
func SUT(f Foo) {
// ...
if 99 == f.Bar(88) {
fmt.Print("ok")
}
}
生成的代码如下
// Code generated by MockGen. DO NOT EDIT.
// Source: exp1/foo.go
// Package mock_foo is a generated GoMock package.
package mock_foo
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockFoo is a mock of Foo interface.
type MockFoo struct {
ctrl *gomock.Controller
recorder *MockFooMockRecorder
}
// MockFooMockRecorder is the mock recorder for MockFoo.
type MockFooMockRecorder struct {
mock *MockFoo
}
// NewMockFoo creates a new mock instance.
func NewMockFoo(ctrl *gomock.Controller) *MockFoo {
mock := &MockFoo{ctrl: ctrl}
mock.recorder = &MockFooMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFoo) EXPECT() *MockFooMockRecorder {
return m.recorder
}
// Bar mocks base method.
func (m *MockFoo) Bar(x int) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bar", x)
ret0, _ := ret[0].(int)
return ret0
}
// Bar indicates an expected call of Bar.
func (mr *MockFooMockRecorder) Bar(x interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bar", reflect.TypeOf((*MockFoo)(nil).Bar), x)
}
// Bar1 mocks base method.
func (m *MockFoo) Bar1(x int) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bar1", x)
ret0, _ := ret[0].(int)
return ret0
}
// Bar1 indicates an expected call of Bar1.
func (mr *MockFooMockRecorder) Bar1(x interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bar1", reflect.TypeOf((*MockFoo)(nil).Bar1), x)
}
我们可以看到,对于每个接口都会生成一个对应的结构体,并且实现了接口的方法。
同时还定义了一个结构体的recoder,recorder实现了同名方法。为什么要定义一个recorder呢?
我们可以看到生成的结构体有两个属性,一个就是全局数据管理的controller,另一个就是recorder
type MockFoo struct {
ctrl *gomock.Controller
recorder *MockFooMockRecorder
}
recorder里面定义了这个结构体的指针
type MockFooMockRecorder struct {
mock *MockFoo
}
gomock的测试分两步
1,写打桩方法:
bar := m.
EXPECT().
Bar(gomock.Eq(99)).
DoAndReturn(func(_ int) int {
time.Sleep(1 * time.Second)
return 101
}).
AnyTimes()
2,进行函数调用:
SUT(tt.args.f)
里面调用了接口的一个函数
if 99 == f.Bar(88)
我们写打桩方法的时候为啥要加上EXPECT()呢?
我们先看下EXPECT()内部的实现
func (m *MockFoo) EXPECT() *MockFooMockRecorder {
return m.recorder
}
其实是返回了生成结构体的recorder对象。
那么就不难理解了,打桩的时候是调用的recorder的方法,函数调用的时候调用的是生成结构体的方法。
下面我们看下函数内部的具体实现
1,打桩方法
// Bar indicates an expected call of Bar.
func (mr *MockFooMockRecorder) Bar(x interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bar", reflect.TypeOf((*MockFoo)(nil).Bar), x)
}
打桩的时候只是将对象和对应的方法注入存储起来。
2,结构体方法
// Bar mocks base method.
func (m *MockFoo) Bar(x int) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bar", x)
ret0, _ := ret[0].(int)
return ret0
}
真正调用的时候是用来controller的call方法进行revoke
下面看下方法注入的具体实现
func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call {
ctrl.T.Helper()
call := newCall(ctrl.T, receiver, method, methodType, args...)
ctrl.mu.Lock()
defer ctrl.mu.Unlock()
ctrl.expectedCalls.Add(call)
return call
}
先构建了一个Call对象,然后将它存储在controller的expectedCalls里面
我们先看下Controller的定义
type Controller struct {
// T should only be called within a generated mock. It is not intended to
// be used in user code and may be changed in future versions. T is the
// TestReporter passed in when creating the Controller via NewController.
// If the TestReporter does not implement a TestHelper it will be wrapped
// with a nopTestHelper.
T TestHelper
mu sync.Mutex
expectedCalls *callSet
finished bool
}
Controller里最重要的属性就是expectedCalls,里面是两个map,map的key是对象和对象对应的方法,值就是Call的指针一个slice,第一个map是期望的调用,第二个map是超过期望调用次数的调用的一个存储
type callSet struct {
// Calls that are still expected.
expected map[callSetKey][]*Call
// Calls that have been exhausted.
exhausted map[callSetKey][]*Call
}
具体看下callSetKey的定义,里面存储了接口和函数名
type callSetKey struct {
receiver interface{}
fname string
}
然后我们看看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{}
}
我们回来看打桩的过程,在构建了Call对象以后呢,会将Call对象存入到expectedCalls,用到的add方法其实就是一个append操作
// Add adds a new expected call.
func (cs callSet) Add(call *Call) {
key := callSetKey{call.receiver, call.method}
m := cs.expected
if call.exhausted() {
m = cs.exhausted
}
m[key] = append(m[key], call)
}
在完成打桩依赖注入后,我们看看具体函数调用的过程,它调了Call方法
func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{} {
ctrl.T.Helper()
// Nest this code so we can use defer to make sure the lock is released.
actions := func() []func([]interface{}) []interface{} {
ctrl.T.Helper()
ctrl.mu.Lock()
defer ctrl.mu.Unlock()
expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args)
if err != nil {
// callerInfo's skip should be updated if the number of calls between the user's test
// and this line changes, i.e. this code is wrapped in another anonymous function.
// 0 is us, 1 is controller.Call(), 2 is the generated mock, and 3 is the user's test.
origin := callerInfo(3)
ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err)
}
// Two things happen here:
// * the matching call no longer needs to check prerequite calls,
// * and the prerequite calls are no longer expected, so remove them.
preReqCalls := expected.dropPrereqs()
for _, preReqCall := range preReqCalls {
ctrl.expectedCalls.Remove(preReqCall)
}
actions := expected.call()
if expected.exhausted() {
ctrl.expectedCalls.Remove(expected)
}
return actions
}()
var rets []interface{}
for _, action := range actions {
if r := action(args); r != nil {
rets = r
}
}
return rets
}
1,通过
ctrl.expectedCalls.FindMatch(receiver, method, args)
2,找到我们打桩的时候注入的方法,然后执行方法,获取返回值
if r := action(args); r != nil
可以看下FindMatch的具体实现
func (cs callSet) FindMatch(receiver interface{}, method string, args []interface{}) (*Call, error) {
key := callSetKey{receiver, method}
// Search through the expected calls.
expected := cs.expected[key]
var callsErrors bytes.Buffer
for _, call := range expected {
err := call.matches(args)
if err != nil {
_, _ = fmt.Fprintf(&callsErrors, " %v", err)
} else {
return call, nil
}
}
// If we haven't found a match then search through the exhausted calls so we
// get useful error messages.
exhausted := cs.exhausted[key]
for _, call := range exhausted {
if err := call.matches(args); err != nil {
_, _ = fmt.Fprintf(&callsErrors, " %v", err)
continue
}
_, _ = fmt.Fprintf(
&callsErrors, "all expected calls for method %q have been exhausted", method,
)
}
if len(expected)+len(exhausted) == 0 {
_, _ = fmt.Fprintf(&callsErrors, "there are no expected calls of the method %q for that receiver", method)
}
return nil, fmt.Errorf(callsErrors.String())
}
其实就是通过key和参数对比,找到注入的实现。
以上是关于gomock 源码分析的主要内容,如果未能解决你的问题,请参考以下文章
Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段