Monday, February 1, 2016

Mockito



http://monkeyisland.pl/2008/01/14/mockito/

http://www.infoq.com/cn/articles/mockito-design
Mock。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。
如果用平常的面向对象的思想来设计API来做同样的事情,估计结果是这样的:
Mockito.returnValueWhen("one", mockedList, "get", 0);
第一个参数描述要返回的结果,第二个参数指定mock对象,第三个参数指定mock方法,后面的参数指定mock方法的参数值。这样的代码,更符合我们看一般代码时候的思路。
但是,把上面的代码跟Mockito的代码进行比较,我们会发现,我们的代码有几个问题:
  1. 不够直观
  2. 对重构不友好
第二点尤其重要。想象一下,如果我们要做重构,把get方法改名叫fetch方法,那我们要把”get”字符串替换成”fetch”,而字符串替换没有编译器的支持,需要手工去做,或者查找替换,很容易出错。而Mockito使用的是方法调用,对方法的改名,可以用编译器支持的重构来进行,更加方便可靠。

明确了Mockito的方案更好之后,我们来看看Mockito的方案是如何实现的。首先我们要知道,Mock对象这件事情,本质上是一个Proxy模式的应用。Proxy模式说的是,在一个真实对象前面,提供一个proxy对象,所有对真实对象的调用,都先经过proxy对象,然后由proxy对象根据情况,决定相应的处理,它可以直接做一个自己的处理,也可以再调用真实对象对应的方法。Proxy对象对调用者来说,可以是透明的,也可以是不透明的。
Java本身提供了构建Proxy对象的API:Java Dynamic Proxy API。Mockito就是用Java提供的Dynamic Proxy API来实现的。
下面我们来看看,到底如何实现文章开头的示例中的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。

总结

通过以上的分析我们可以看到,Mockito在设计时实际上有意地使用了方法的“副作用”,在返回值之外,还保存了方法调用的信息,进而在最后利用这些信息,构建出一个mock。而这些信息的保存,是对Mockito的用户完全透明的。这是一个经典的“反模式”的使用案例。“模式”告诉我们,在设计方法的时候,应该避免副作用,一个方法在被调用时候,除了return返回值之外,不应该产生其他的状态改变,尤其不应该有“意料之外”的改变。但Mockito完全违反了这个原则,Mockito的静态方法Mockito.anyString(), mockInstance.method(), Mockito.when(), thenReturn(),这些方法,在背后都有很大的“副作用” —— 保存了调用者的信息,然后利用这些信息去完成任务。这就是为什么Mockito的代码一开始会让人觉得奇怪的原因,因为我们平时不这样写代码。
然而,作为一个Mocking框架,这个“反模式”的应用实际上是一个好的设计。就像我们前面看到的,它带来了非常简单的API,以及编译安全,可重构等优良特性。违反直觉的方法调用,在明白其原理和一段时间的熟悉之后,也显得非常的自然了。设计的原则,终究是为设计目标服务的,原则在总结出来之后,不应该成为僵硬的教条,根据需求灵活地应用这些原则,才能达成好的设计。在这方面,Mockito堪称一个经典案例。
动手编写 Mockito
https://coding.net/u/tanhe123/p/MockitoSamples
初次使用 Mockito,能够感受到它的神奇,尤其是这样的语法:
when(cal.add(0, 1)).thenReturn(1);
Mockito 会把它理解成,当 cal 调用 add 方法且参数为 0 和 1 时,则返回 1。
我们知道,java 中的程序调用是以栈的形式实现的,对于 when 方法,add 方法的调用对它是不可见的。when 能接收到的,只有 add 的返回值。

那么 Mockito 是如何实现的呢?

我们知道,Mock 使用了代理模式。我们操作的 cal,实际上是继承了 Calculate 的代理类的实例,我们把它称为 proxy 对象。因此对于 cal 的所有操作,都会先经过 proxy 对象。
Mocktio 就是通过这种方式,拦截到了 cal 的所有操作。只要在调用 cal 的方法时,将该方法存放起来,然后在调用 thenReturn 为其设置返回值就可以了。
该 Mockito 需要支持的特性有 mock、stub、spy。
既然要实现 mock,我们要知道,一个对象被 mock 后,有什么特征。
在单元测试中,我们要测试的模块可能依赖一些不易构造或比较复杂的对象,因此,mock 要支持通过接口、具体类构造 mock 对象。被 mock 的对象只是“假装”调用了该方法,然后“应付”的返回“空值”就可以了。
我们通过 cglib 来实现,实现代码很简单。
public static <T> T mock(Class<T> clazz) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(new MockInterceptor());
    return (T) enhancer.create();
}

private static class MockInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        return null;
    }
}
测试用例我们可以这么写
Calculate cal = mock(Calculate.class);
Assert.assertEquals(0, cal.add(0, 1));

接下来我们来实现 stub。stub 一个方法,其实就是指定该方法在具体参数下的返回值。而且该返回值无论经过多少次调用都是不变的,除非再次 stub 该方法,用新的返回值将原来的替换掉。

首先,我们定义一个类,用来表示对一个函数的调用。
public class Invocation {
    private final Object mock;

    private final Method method;

    private final Object[] arguments;

    private final MethodProxy proxy;

    // 省略其它不重要代码...
}
接下来,在 MockInterceptor 类中,需要做两个操作。
  1. 为了设置方法的返回值,需要存放对方法的引用(lastInvocation)
  2. 调用方法时,检查是否已经设置了该方法的返回值(results)。如果设置了,则返回该值。
实现代码如下:
public class Mockito {
private static Map<Invocation, Object> results = new HashMap<Invocation, Object>();
private static Invocation lastInvocation;
public static <T> T mock(Class<T> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(new MockInterceptor());
return (T) enhancer.create();
}
private static class MockInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Invocation invocation = new Invocation(proxy, method, args, proxy);
lastInvocation = invocation;
if (results.containsKey(invocation)) {
return results.get(invocation);
}
return null;
}
}
public static <T> When<T> when(T o) {
return new When<T>();
}
public static class When<T> {
public void thenReturn(T retObj) {
results.put(lastInvocation, retObj);
}
}
}
可以通过下面测试下效果:
Calculate cal = mock(Calculate.class);
when(cal.add(0, 1)).thenReturn(1);
Assert.assertEquals(1, cal.add(0, 1));

第三步,实现 spy

如果说 mock 的处理是返回“空值”,那么 spy 的处理就是通过代理对象调用真实方法了。
我们使用 cglib 提供的方法,使得 spy 生成的代理类,默认调用真实方法。
public Object callRealMethod() {
    try {
        return this.proxy.invokeSuper(obj, arguments);
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }

    return null;
}
就这么简单,spy就实现了,可以使用下面的代码测试
Calculate cal = spy(Calculate.class);
Assert.assertEquals(3, cal.add(1, 2));
when(cal.add(1, 2)).thenReturn(0);
Assert.assertEquals(0, cal.add(1, 2));
http://heipark.iteye.com/blog/1496603
  1. List<String> list = new LinkedList<String>();  
  2. List<String> spy = spy(list);  
  3. when(spy.size()).thenReturn(100);  
  4.   
  5. spy.add("one");  
  6. spy.add("two");  
  7.   
  8. assertEquals(spy.get(0), "one");  
  9. assertEquals(100, spy.size());  
spy的原理是,如果不打桩默认都会执行真实的方法,如果打桩则返回桩实现。
可以看出spy.size()通过桩实现返回了值100,而spy.get(0)则返回了实际值。
严重注意:使用spy的桩实现实际还是会调用stub的方法,只是返回了stub的值,验证代码如下:
  1. Test  
  2. public void spyTest() {  
  3.     Jack spyJack = spy(new Jack());  
  4.     when(spyJack.go()).thenReturn(false);  
  5.   
  6.     assertFalse(spyJack.go());  
  7. }  
  8.   
  9. class Jack {  
  10.     public boolean go() {  
  11.         System.out.println("I say go go go!!");  
  12.         return true;  
  13.     }  
  14. }  
go方法的返回值的确返回的桩实现,但是通过console看到go()方法的确输出了文字。
★ 批注:又捣鼓了一下,发下使用语句“doReturn(1111).when(spyJack).go();” 是好使的,这是一个陷阱,使用的时候要注意。 // doReturn it doesn't check return value type
CallRealMethod
  1. @Test  
  2. public void callRealMethodTest() {  
  3.     Jerry jerry = mock(Jerry.class);  
  4.   
  5.     doCallRealMethod().when(jerry).goHome();  
  6.     doCallRealMethod().when(jerry).doSomeThingB();  
  7.   
  8.     jerry.goHome();  
  9.   
  10.     verify(jerry).doSomeThingA();  
  11.     verify(jerry).doSomeThingB();  
  12. }  
  13.   
  14. class Jerry {  
  15.     public void goHome() {  
  16.         doSomeThingA();  
  17.         doSomeThingB();  
  18.     }  
  19.   
  20.     // real invoke it.  
  21.     public void doSomeThingB() {  
  22.         System.out.println("good day");  
  23.   
  24.     }  
  25.   
  26.     // auto mock method by mockito  
  27.     public void doSomeThingA() {  
  28.         System.out.println("you should not see this message.");  
  29.   
  30.     }  
  31. }  
    通过代码可以看出Jerry是一个mock对象, goHome()和doSomeThingB()是使用了实际调用技术,而doSomeThingA()被mockito执行了默认的answer行为(这里是个void方法,so,什么也不干)。

总结:
    spy和callrealmethod都可以实现部分mock,唯一不同的是通过spy做的桩实现仍然会调用实际方法(我都怀疑这是不是作者的bug)。
   ★ 批注:spy方法需要使用doReturn方法才不会调用实际方法。

http://blog.csdn.net/jamesdoctor/article/details/50019103
http://www.importnew.com/15353.html
想要测试一个抽象类,有什么好办法可以不用真正继承这个类就可以进行测试吗?如果使用Mockito框架又要怎么做?
以下的建议可以让你不用创建“实际”的子类来测试抽象类 —— Mock就是子类。
使用Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS),然后就可以mock任何被调用的抽象method。
这比使用spy更加简洁,因为spy需要实例,也就是说你必须为你的抽象类创建一个可以实例化的子类。

Labels

Review (572) System Design (334) System Design - Review (198) Java (189) Coding (75) Interview-System Design (65) Interview (63) Book Notes (59) Coding - Review (59) to-do (45) Linux (43) Knowledge (39) Interview-Java (35) Knowledge - Review (32) Database (31) Design Patterns (31) Big Data (29) Product Architecture (28) MultiThread (27) Soft Skills (27) Concurrency (26) Cracking Code Interview (26) Miscs (25) Distributed (24) OOD Design (24) Google (23) Career (22) Interview - Review (21) Java - Code (21) Operating System (21) Interview Q&A (20) System Design - Practice (20) Tips (19) Algorithm (17) Company - Facebook (17) Security (17) How to Ace Interview (16) Brain Teaser (14) Linux - Shell (14) Redis (14) Testing (14) Tools (14) Code Quality (13) Search (13) Spark (13) Spring (13) Company - LinkedIn (12) How to (12) Interview-Database (12) Interview-Operating System (12) Solr (12) Architecture Principles (11) Resource (10) Amazon (9) Cache (9) Git (9) Interview - MultiThread (9) Scalability (9) Trouble Shooting (9) Web Dev (9) Architecture Model (8) Better Programmer (8) Cassandra (8) Company - Uber (8) Java67 (8) Math (8) OO Design principles (8) SOLID (8) Design (7) Interview Corner (7) JVM (7) Java Basics (7) Kafka (7) Mac (7) Machine Learning (7) NoSQL (7) C++ (6) Chrome (6) File System (6) Highscalability (6) How to Better (6) Network (6) Restful (6) CareerCup (5) Code Review (5) Hash (5) How to Interview (5) JDK Source Code (5) JavaScript (5) Leetcode (5) Must Known (5) Python (5)

Popular Posts