徒手撸一个Mock框架(一)——如何创建一个mock对象
徒手撸一个Mock框架(二)——如何创建final类的代理
徒手撸一个Mock框架(三)—— JUnit4Runner+ClassLoader=为所欲为
上一篇我们的StupidMock
已经解决了创建各种mock对象的问题。今天我们来解决方法调用mock
的问题。
先来看一个例子。在使用Mockito
的时候,如果想要mock一个对象的行为,一般的用法是:
在when...thenReturn
之后,无论原来的方法原本的实现是什么样子,如果传入a,b
两个参数值,那么就会返回固定的Hello
。
今天我们要实现的就是这个东西。
关键点分析
我们先来思考一下,究竟需要做一些什么。从最抽象的程度来说,一个方法调用可以描述为某个对象调用一个方法,参数是XXX,最后响应是XXX。
所以,我们需要解决四个问题:
- 确定对象;
- 确定方法;
- 方法调用参数;
- 返回值;
如果不考虑用户体验的话,我们可以直接让用户配置一大堆的东西,把我们所需要的信息都配置过来,我们傻瓜式的根据配置跑一下就可以了。
但是在考虑了用户体验的时候,就不能这么做了。
所以,前三个问题也成了很大的问题。在前面的例子里,when
接收的是doSomething
调用之后的返回值,所以肯定不能在when
方法里面获取到对象和调用方法的信息。于是我们的选择就只剩下了在调用doSomething
的时候将内容保存下来。
而在thenReturn
的时候,这个return
的内容,就直接是受到我们控制的,所以很好解决,直接在StupidMock
里面保存起来就可以。
于是我们要做的事情就是:
- 当
mock
对象调用某个方法的时候,保存下这次调用的对象,参数信息,以及方法; - 当调用
thenReturn
的时候,将参数也保存下来; - 将前面保存的信息关联起来,放到一起。
最终保存的东西,我们称为stub
。
所以当用户发起一次真的调用的时候,我们要做的就是,从所有创建的stub
里面,找到匹配的那个,将stub
中设置的返回值返回。
获取对象、方法和参数
前面的分析里面提到,我们只能在doSomething
方法里面收集对象、方法和参数。
现在我们要考虑的问题是:
- 如何收集;
- 放在哪里,怎么获取;
第一个问题理论上来说,并不复杂,因为我们创建的mock对象,是利用cglib
来创建的,我们可以在创建代理的时候,传入callback
参数,这个callback
就是用来保存这一次的调用对象、方法和参数;
第二个问题,更加多的是设计的问题。我们可以直接把这些信息放在mock
对象内部,然后在when
方法里面将它取出来。有一个问题是,我们无法区别两种调用,即无法区别用户是在创建一个stub
还是真的在执行一个调用。
解决办法就是,我们都处理。既认为这是一次调用,也认为这是一个创建stub。
- 作为一次调用,我们将从所有已经注册的
stub
里面找到匹配的,返回注册的返回值; - 作为一次创建
stub
的步骤之一,我们将保存这次调用的上下文;
为了统一处理,我们会在创建mock
对象的时候,加入一个默认的stub
,该stub
就是各种类型的默认值。如基本类型则是基本类型对应的默认值,如果是对象则返回null
。
Callback实现
上图是我们的mock对象时候使用的方法,很容易发现,关键点就在于实现MethodInterceptor
接口,并且注册进去。
所以我们先实现一个自己的MethodInterceptor
。
MethodInterceptor
的实现关键是MockObjectSkeleton
和ThreadSafeStubBuilder
。本质上来说,这个实现只是一个“胶合层”,负责将StupidMock
和cglib
粘在一起。虽然理论上来说,我可以将MockObjectSkeleton
和ThreadSafeBuilder
的逻辑都直接写在其中,但是这会让我们的实现过于臃肿。
这里还有一个将Object
转化为ArgMatcher
的过程。这是因为,在我们的stub
里面,并不能直接使用这个参数,而是要保存一些参数匹配条件。
比如说有些时候我们的写法可能是:when(obj.doSomething(any(),any()).then(...)
。
于是我们对应的StupidMock
就变成了:
最终的使用效果类似:
MockObjectSkeleton
MockObjectSkeleton
在这里更加接近一个容器的概念。它里面负责放置stub
实例,并且从stub
里面找出一个来,执行stub
,并返回对象。
这里有一个地方需要注意的是,我采用的是一个List
来保存stub
。并且每次添加的时候都是将stub
加在队列前。
这是一个非常粗糙的做法:
- 按照我们的匹配原则,如果我们设定了两个
stub
,对于某一次方法调用,那么后一个设定的stub
就会覆盖掉前一个,作为结果返回; - 对于一个方法来说,可以有很多
stub
,并且我们没有提供删除某些stub
的方法;
可以考虑用一个
Map
结构来取代List
,以实现单个方法只会有一个stub
。
StubBuilder
StubBuilder
则是另外一个关键点。上图的接口定义其实很好理解,需要额外解释的就是addOberver
方法。
这是一个观察者模式的应用。它主要是为了解决MockObjectSkeleton
需要维护stub
,而创建stub
则是在StubBuilder
里面完成的。除此以外,一种可取得做法我们可以将MockObjectSkeleton
的实例传入StubBuilder
实例,但是这意味着两者将强耦合在一起,这是我所不希望的。所以设计了一个BuildingStubObserver
接口,单纯就是为了解耦,以及扩展性。
现在要来看最为绕的地方了,就是ThreadSafeStubBuilder
。在此之前,我要先分析一下我们面对的困难时什么。
在我们的模型里面,牵涉到了simpleObject
——即mock
对象,StupidMockMethodInterceptorAdaptorImpl
实例——在创建mock
对象的时候创建,StupidMock
——它的静态方法,还有核心stub
实例。
这意味着,我们需要在这些所有牵涉到的对象或者类中共享stub
实例的创建过程。我们要在StupidMockMethodInterceptorAdaptorImpl
里面创建StubBuilder
并且这个StubBuilder
要在StupidMock
里面被返回。
于是关键问题是,StupidMockMethodInterceptorAdaptorImpl
怎么把StubBuilder
传递给StupidMock
。
答案是通过某个共享的中间变量。
这个共享中间变量就是ThreadSafeStubBuilder
。
实际上,我们是利用了ThreadSafeStubBuilder
里面的静态变量stubBuilder
来实现这种共享的。stubBuilder
利用了Java的ThreadLocal
特性,来保证线程安全。
所以,无论是在StupidMockMethodInterceptorAdaptorImpl
里面new ThreadSafeStubBuilder
还是在StupidMock
里面new ThreadSafeStubBuilder
,它们实际上操作的都是同一个StubBuilder
。
IStub, Answer和ArgMatcher
IStub
的定义只有两个方法,一个是判断自身与某一次实际调用是否匹配,如果匹配的话,则意味着要使用该stub
实例,于是调用getAnswer
得到answer
实例.
Answer
接口被定义为函数式接口,里面只有一个方法。它代表的就是用户想要在实际调用时候mock
的动作。
在IStub
的默认实现DefaultStubImpl
之中,match
方法的实现如下:
其逻辑最重要的部分就是参数匹配,这是利用ArgMatcher
来进行的:
注意到的是,在StupidMockMethodInterceptorAdaptorImpl
里面我们只使用了一种实现,就是FixedValueArgMatcherImpl
。因为这一篇文章不讨论复杂的参数匹配问题,我会在下一篇讨论这个问题。FixedValueArgMatcherImpl
就是指匹配特定值,其实现是:
设计总结
这一篇文章,其实没有涉及太多复杂的技术,更加多的是设计上的问题。我在弄这个东西的时候,很多时候都抄袭了Mockito
的东西,不过将里面复杂的东西都去掉了。
但是核心问题,或者说,关键点,我自认为还是保留下来了。
这个核心问题就是我所谈及的,如果让StubBuilder
在各个地方共享,并且能够保证线程安全,以及mock
的正确性。
现在我来列举一下这个设计的核心接口。这些接口定义了整个系统的运作方式,堪称灵魂。
第一个接口是IStub
接口。定义了一个stub
应该知晓自己是否能够被某次调用所使用,并且定义了该如何“应答”这次调用。
这就是Answer
接口。Answer
接口解决了在mock
中做什么的问题。在Mockito
里面那些复杂then
, thenReturn
, thenThrow
之类的,都可以实现Answer
接口以达成。
而另外一个接口StubBuilder
接口,则定义了一个stub
该如何被创建出来。它是将cglib
,mock
对象,和StupidMock
以及其余(后续会有)东西结合起来的关键。
至于剩下的东西,不过是一些边角之物。