Java测试框架Mockito源码分析

1.Mockito简介

测试驱动的开发(Test Driven Design, TDD)要求咱们先写单元测试,再写实现代码。在写单元测试的过程当中,一个很广泛的问题是,要测试的类会有不少依赖,这些依赖的类/对象/资源又会有别的依赖,从而造成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。
所幸,咱们有一个应对这个问题的办法:Mock。简单地说就是对测试的类所依赖的其余类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,而后提供给被测试对象使用。被测试对象像使用真的对象同样使用它们。用这种方式,咱们能够把测试的目标限定于被测试对象自己,就如同在被测试对象周围作了一个划断,造成了一个尽可能小的被测试目标。Mock的框架有不少,最为知名的一个是Mockito,这是一个开源项目,使用普遍。官网:http://site.mockito.org/java

2.Mockito框架设计

首先咱们要知道,Mock对象这件事情,本质上是一个Proxy模式的应用。Proxy模式说的是,在一个真实对象前面,提供一个proxy对象,全部对真实对象的调用,都先通过proxy对象,而后由proxy对象根据状况,决定相应的处理,它能够直接作一个本身的处理,也能够再调用真实对象对应的方法。示例:安全

代码中的注释描述了代码的逻辑:先建立mock对象,而后设置mock对象上的方法get,指定当get方法被调用,而且参数为0的时候,返回”one”;而后,调用被测试方法(被测试方法会调用mock对象的get方法);最后进行验证。逻辑很好理解,可是初次看到这个代码的人,会以为有点儿奇怪,总感受这个代码跟通常的代码不太同样。让咱们仔细想一想看,下面这个代码:bash

// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回”one”
Mockito.when(mockedList.get(0)).thenReturn(“one”);框架

public class MockDemo { // 建立mock对象 List<String> mockedList = Mockito.mock(List.class); @Before public void setUp(){ // 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one" Mockito.when(mockedList.get(0)).thenReturn("one"); } @Test public void mockDemoTest(){ // 使用mock对象 - 会返回前面设置好的值"one",即使列表其实是空的 String str = mockedList.get(0); Assert.assertTrue("one".equals(str)); Assert.assertTrue(mockedList.size() == 0); } }

 

若是按照通常代码的思路去理解,是要作这么一件事:调用mockedList.get方法,传入0做为参数,而后获得其返回值(一个object),而后再把这个返回值传给when方法,而后针对when方法的返回值,调用thenReturn。好像有点不通?mockedList.get(0)的结果,语义上是mockedList的一个元素,这个元素传给when是表示什么意思?因此,咱们不能按照寻常的思路去理解这段代码。实际上这段代码要作的是描述这么一件事情:当mockedList的get方法被调用,而且参数的值是0的时候,返回”one”。很不寻常,对吗?若是用日常的面向对象的思想来设计API来作一样的事情,估计结果是这样的:函数

Mockito.returnValueWhen(“one”, mockedList, “get”, 0);
第一个参数描述要返回的结果,第二个参数指定mock对象,第三个参数指定mock方法,后面的参数指定mock方法的参数值。这样的代码,更符合咱们看通常代码时候的思路。单元测试

可是,把上面的代码跟Mockito的代码进行比较,咱们会发现,咱们的代码有几个问题:
1.不够直观
2.对重构不友好
第二点尤为重要。想象一下,若是咱们要作重构,把get方法更名叫fetch方法,那咱们要把”get”字符串替换成”fetch”,而字符串替换没有编译器的支持,须要手工去作,或者查找替换,很容易出错。而Mockito使用的是方法调用,对方法的更名,能够用编译器支持的重构来进行,更加方即可靠。测试

3.实现分析

明确了Mockito的方案更好以后,咱们来看看Mockito的方案是如何实现的。首先咱们要知道,Mock对象这件事情,本质上是一个Proxy模式的应用。Proxy模式说的是,在一个真实对象前面,提供一个proxy对象,全部对真实对象的调用,都先通过proxy对象,而后由proxy对象根据状况,决定相应的处理,它能够直接作一个本身的处理,也能够再调用真实对象对应的方法。Proxy对象对调用者来讲,能够是透明的,也能够是不透明的。fetch

Java自己提供了构建Proxy对象的API:Java Dynamic Proxy API,而Mockito是用Cglib来实现的。
下面看下运行时期Cglib生成的Mock代理对象的.class文件是怎么样的this

public class List$$EnhancerByMockitoWithCGLIB$$d85c0201 implements List, Factory { ........ private static final Method CGLIB$get$9$Method; ........ public final boolean removeAll(Collection var1) { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if(this.CGLIB$CALLBACK_0 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } if(var10000 != null) { Object var2 = var10000.intercept(this, CGLIB$removeAll$26$Method, new Object[]{var1}, CGLIB$removeAll$26$Proxy); return var2 == null?false:((Boolean)var2).booleanValue(); } else { return super.removeAll(var1); } } final boolean CGLIB$retainAll$27(Collection var1) { return super.retainAll(var1); } .......... case -208030418: if(var10000.equals("get(I)Ljava/lang/Object;")) { return CGLIB$get$9$Proxy; } break; ......... } 

 

能够看到Mokito利用Cglib为List的全部方法都作了Mock实现,可是咱们只对get方法作了Stub,因此只用关注这些代码spa

CGLIB$get$9$Proxy = MethodProxy.create(var1, var0, "(I)Ljava/lang/Object;", "get", "CGLIB$get$9"); case -208030418: if(var10000.equals("get(I)Ljava/lang/Object;")) { return CGLIB$get$9$Proxy; } break;

 

 

看到第一句是否是和我上面说的面向对象的写法很像
这里写图片描述
下面咱们来看看,到底如何实现文章开头的示例中的API。若是咱们仔细分析,就会发现,示例代码最难理解的部分是创建Mock对象(proxy对象),并配置好mock方法(指定其在什么状况下返回什么值)。只要设置好了这些信息,后续的验证是比较容易理解的,由于全部的方法调用都通过了proxy对象,proxy对象能够记录全部调用的信息,供验证的时候去检查。下面咱们重点关注stub配置的部分,也就是咱们前面提到过的这一句代码:

// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one" Mockito.when(mockedList.get(0)).thenReturn("one");

 

当when方法被调用的时候,它其实是没有办法获取到mockedList上调用的方法的名字(get),也没有办法获取到调用时候的参数(0),它只能得到mockedList.get方法调用后的返回值,而根本没法知道这个返回值是经过什么过程获得的。这就是普通的java代码。为了验证咱们的想法,咱们实际上能够把它重构成下面的样子,不改变它的功能:

// 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one" String str = mockedList.get(0); Mockito.when(str).thenReturn("one");

 

这对Java开发者来讲是常识,那么这个常识对Mockito是否还有效呢。咱们把上面的代码放到Mockito测试中实际跑一遍,结果跟前面的写法是同样的,证实了常识依然有效。

有了上面的分析,咱们基本上能够猜出来Mockito是使用什么方式来传递信息了 —— 不是用方法的返回值,而是用某种全局的变量。当get方法被调用的时候(调用的其实是proxy对象的get方法),代码实际上保存了被调用的方法名(get),以及调用时候传递的参数(0),而后等到thenReturn方法被调用的时候,再把”one”保存起来,这样,就有了构建一个stub方法所需的全部信息,就能够构建一个stub方法了。

上面的设想是否正确呢?Mockito是开源项目,咱们能够从代码当中验证咱们的想法。下面是MockHandlerImpl.handle()方法的代码。代码来自Mockito在Github上的代码。

public Object handle(Invocation invocation) throws Throwable {
     if (invocationContainerImpl.hasAnswersForStubbing()) { ... } ... InvocationMatcher invocationMatcher = matchersBinder.bindMatchers( mockingProgress.getArgumentMatcherStorage(), invocation ); mockingProgress.validateState(); // if verificationMode is not null then someone is doing verify() if (verificationMode != null) { ... } // prepare invocation for stubbing invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher); OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainerImpl); mockingProgress.reportOngoingStubbing(ongoingStubbing); ... }

 

注意第1行,第6-9行,能够看到方法调用的信息(invocation)对象被用来构造invocationMatcher对象,而后在第19-21行,invocationMatcher对象最终传递给了ongoingStubbing对象。完成了stub信息的保存。这里咱们忽略了thenReturn部分的处理。有兴趣的同窗能够本身看代码研究。

看到这里,咱们能够得出结论,mockedList对象的get方法的实际处理函数是一个proxy对象的方法(最终调用MockHandlerImpl.handle方法),这个handle方法除了return返回值以外,还作了大量的处理,保存了stub方法的调用信息,以便以后能够构建stub。

4.总结

经过以上的分析咱们能够看到,Mockito在设计时实际上有意地使用了方法的“反作用”,在返回值以外,还保存了方法调用的信息,进而在最后利用这些信息,构建出一个mock。而这些信息的保存,是对Mockito的用户彻底透明的。“模式”告诉咱们,在设计方法的时候,应该避免反作用,一个方法在被调用时候,除了return返回值以外,不该该产生其余的状态改变,尤为不该该有“意料以外”的改变。但Mockito彻底违反了这个原则,Mockito的静态方法Mockito.anyString(), mockInstance.method(), Mockito.when(), thenReturn(),这些方法,在背后都有很大的“反作用” —— 保存了调用者的信息,而后利用这些信息去完成任务。这就是为何Mockito的代码一开始会让人以为奇怪的缘由,由于咱们平时不这样写代码。

然而,做为一个Mocking框架,这个“反模式”的应用其实是一个好的设计。就像咱们前面看到的,它带来了很是简单的API,以及编译安全,可重构等优良特性。违反直觉的方法调用,在明白其原理和一段时间的熟悉以后,也显得很是的天然了。设计的原则,终究是为设计目标服务的,原则在总结出来以后,不该该成为僵硬的教条,根据需求灵活地应用这些原则,才能达成好的设计。在这方面,Mockito堪称一个经典案例。

相关文章
相关标签/搜索