1.Mockito简介

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

2.Mockito框架设计

首先我们要知道,Mock对象这件事情,本质上是一个Proxy模式的应用。Proxy模式说的是,在一个真实对象前面,提供一个proxy对象,所有对真实对象的调用,都先经过proxy对象,然后由proxy对象根据情况,决定相应的处理,它可以直接做一个自己的处理,也可以再调用真实对象对应的方法。示例:

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

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

  1. public class MockDemo {
  2. // 创建mock对象
  3. List<String> mockedList = Mockito.mock(List.class);
  4. @Before
  5. public void setUp(){
  6. // 设置mock对象的行为 - 当调用其get方法获取第0个元素时,返回"one"
  7. Mockito.when(mockedList.get(0)).thenReturn("one");
  8. }
  9. @Test
  10. public void mockDemoTest(){
  11. // 使用mock对象 - 会返回前面设置好的值"one",即便列表实际上是空的
  12. String str = mockedList.get(0);
  13. Assert.assertTrue("one".equals(str));
  14. Assert.assertTrue(mockedList.size() == 0);
  15. }
  16. }

如果按照一般代码的思路去理解,是要做这么一件事:调用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对象对调用者来说,可以是透明的,也可以是不透明的。

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

  1. public class List$$EnhancerByMockitoWithCGLIB$$d85c0201 implements List, Factory {
  2. ........
  3. private static final Method CGLIB$get$9$Method;
  4. ........
  5. public final boolean removeAll(Collection var1) {
  6. MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
  7. if(this.CGLIB$CALLBACK_0 == null) {
  8. CGLIB$BIND_CALLBACKS(this);
  9. var10000 = this.CGLIB$CALLBACK_0;
  10. }
  11. if(var10000 != null) {
  12. Object var2 = var10000.intercept(this, CGLIB$removeAll$26$Method, new Object[]{var1}, CGLIB$removeAll$26$Proxy);
  13. return var2 == null?false:((Boolean)var2).booleanValue();
  14. } else {
  15. return super.removeAll(var1);
  16. }
  17. }
  18. final boolean CGLIB$retainAll$27(Collection var1) {
  19. return super.retainAll(var1);
  20. }
  21. ..........
  22. case -208030418:
  23. if(var10000.equals("get(I)Ljava/lang/Object;")) {
  24. return CGLIB$get$9$Proxy;
  25. }
  26. break;
  27. .........
  28. }

可以看到Mokito利用Cglib为List的所有方法都做了Mock实现,但是我们只对get方法做了Stub,所以只用关注这些代码

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

看到第一句是不是和我上面说的面向对象的写法很像

下面我们来看看,到底如何实现文章开头的示例中的API。如果我们仔细分析,就会发现,示例代码最难理解的部分是建立Mock对象(proxy对象),并配置好mock方法(指定其在什么情况下返回什么值)。只要设置好了这些信息,后续的验证是比较容易理解的,因为所有的方法调用都经过了proxy对象,proxy对象可以记录所有调用的信息,供验证的时候去检查。下面我们重点关注stub配置的部分,也就是我们前面提到过的这一句代码:

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

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

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

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

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

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

  1. public Object handle(Invocation invocation) throws Throwable {
  2. if (invocationContainerImpl.hasAnswersForStubbing()) {
  3. ...
  4. }
  5. ...
  6. InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
  7. mockingProgress.getArgumentMatcherStorage(),
  8. invocation
  9. );
  10. mockingProgress.validateState();
  11. // if verificationMode is not null then someone is doing verify()
  12. if (verificationMode != null) {
  13. ...
  14. }
  15. // prepare invocation for stubbing invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);
  16. OngoingStubbingImpl<T> ongoingStubbing =
  17. new OngoingStubbingImpl<T>(invocationContainerImpl);
  18. mockingProgress.reportOngoingStubbing(ongoingStubbing);
  19. ...
  20. }

注意第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堪称一个经典案例。

Java测试框架Mockito源码分析的更多相关文章

  1. java线程池ThreadPoolExector源码分析

    java线程池ThreadPoolExector源码分析 今天研究了下ThreadPoolExector源码,大致上总结了以下几点跟大家分享下: 一.ThreadPoolExector几个主要变量 先 ...

  2. 框架-spring源码分析(一)

    框架-spring源码分析(一) 参考: https://www.cnblogs.com/heavenyes/p/3933642.html http://www.cnblogs.com/BINGJJF ...

  3. 框架-springmvc源码分析(二)

    框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...

  4. 框架-springmvc源码分析(一)

    框架-springmvc源码分析(一) 参考: http://www.cnblogs.com/heavenyes/p/3905844.html#a1 https://www.cnblogs.com/B ...

  5. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  6. Java8集合框架——LinkedList源码分析

    java.util.LinkedList 本文的主要目录结构: 一.LinkedList的特点及与ArrayList的比较 二.LinkedList的内部实现 三.LinkedList添加元素 四.L ...

  7. 高性能网络I/O框架-netmap源码分析

    from:http://blog.chinaunix.net/uid-23629988-id-3594118.html 博主这篇文章写的很好 感觉很有借签意义 值得阅读 高性能网络I/O框架-netm ...

  8. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  9. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

随机推荐

  1. static的应用

    静态变量.静态代码块.静态方法.非静态方法的区别: 执行顺序:静态变量和静态代码块是按照先后顺序执行的,即在类加载的时候就执行的,属于自动执行的.使用场景:就是一些全局常量,在开始的时候就需要加载的. ...

  2. ElasticStack系列之八 & _source 字段

    有很多人会有这样的一个疑问: _source字段存储的是索引的原始内容,那 store 属性的设置是为何呢?elasticsearch 为什么要把 store 的默认取值设置为 no?设置为 yes ...

  3. Java基础-IO流对象之字节流(Stream)

    Java基础-IO流对象之字节流(Stream) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 在前面我分享的笔记中,我们一直都是在操作文件或者文件夹,并没有给文件中写任何数据.现 ...

  4. addEventListener() 事件监听

    addEventListener() 用于向指定元素添加事件. 可以向一个元素添加多次事件或者多次不同事件,后面的事件是不会覆盖前面的. 语法: element.addEventListener(ev ...

  5. Excel 中 VLOOKUP() 函数小结

    应用场景: 数据仓库上游源系统的数据库表变更,现在需要拆分一部分数据出来,单独放到一张新表中.假设原表为A,新表为B,B表和A表结构大部分一样,只有字段的前缀不同,那么我们如何找出到底有哪些字段不同呢 ...

  6. 关于构造IOCTL命令的学习心得

    在编写ioctl代码之前,需要选择对应不同命令的编号.为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一,这种错误匹配并不是不会发生,程序可能发现自己正在试图对FIFO和audio等这类非 ...

  7. css table 合并单元格

    1. css table 合并单元格 colspan:合并列, rowspan:合并行, 合并行的时候,比如rowspan="2",它的下一行tr会少一列: 合并列的时候,比如co ...

  8. JS中双等号“==”使用情况

    1.判断一个对象的值是否存在 var obj = {}; if(obj.a == null) { // todo } 2. 判断函数入参是否存在 function (a, b) { if(a == n ...

  9. JavaScript中replace()方法的第二个参数解析

    语法 string.replace(searchvalue,newvalue) 参数值 searchvalue 必须.规定子字符串或要替换的模式的 RegExp 对象.请注意,如果该值是一个字符串,则 ...

  10. mongo ttl索引

    db.log_events.find()                                     # 查找log_events里的所有数据   db.log_events.create ...