Android单元测试之Mockito

在博客Android单元测试之JUnit4中,我们简单地介绍了:什么是单元测试,为什么要用单元测试,并展示了一个简单的单元测试例子。在文章中,我们只是展示了对有返回类型的目标public方法进行了单元测试,但是对于返回类型为void的public方法,又是如何进行单元测试呢?往往是验证目标方法中的某个对象的某个方法是否得到了调用,或者验证目标方法中的某个对象的某个状态是否发生改变,以此来验证目标方法是否按照我们想要的逻辑进行调用。

此外在写单元测试的过程中,一个很普遍的问题是,要测试的目标类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。

所幸,我们有一个应对这个问题的办法:Mock。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。

接下来主角登场了,那就是Mockito测试框架。

Mockito是什么

Mockito是一套非常强大的测试框架,被广泛的应用于Java程序的unit test中。相比于EasyMock框架,Mockito使用起来简单,学习成本很低,而且具有非常简洁的API,测试代码的可读性很高。

在测试环境中,通过Mockito来mock出其他的依赖对象,用来替换真实的对象,使得待测的目标方法被隔离起来,避免一些外界因素的影响和依赖,能在我们预设的环境中执行,以达到两个目的:

  1. 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等;
  2. 指定这个对象的某些方法的行为,返回特定的值,或是执行特定的动作;

Mockito初级使用

首先在Gradle配置如下:

repositories { 
    jcenter() 
}
dependencies { 
    testCompile 'junit:junit:4.12'
    testCompile "org.mockito:mockito-core:1.10.19"
}

示例如下:

import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;

import java.util.List;

public class ListTest {

    @Test
    public void testGet() throws Exception {
        // 创建mock对象
        List mockedList = Mockito.mock(List.class);

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

        // 使用mock对象 - 会返回前面设置好的值"one",即便列表实际上是空的
        String str = (String) mockedList.get(0);

        Assert.assertTrue("one".equals(str));
        Assert.assertTrue(mockedList.size() == 0);

        // 验证mock对象的get方法被调用过,而且调用时传的参数是0
        Mockito.verify(mockedList).get(0);
    }

}

代码中的注释描述了代码的逻辑:先创建mock对象,然后设置mock对象上的方法get,指定当get方法被调用,并且参数为0的时候,返回”one”;然后,调用被测试方法(被测试方法会调用mock对象的get方法);最后进行验证。

通过上面的例子,我们可以初步了解到,在Mockito框架中,

  1. 通过Mockito.mock()方法来mock出对象来,这个对象可以是目标类的外界依赖对象,如List mockedList = Mockito.mock(List.class);
  2. 通过Mockito.when().thenReturn()方法为某个mock对象的方法指定返回值,以便执行特定的动作,如Mockito.when(mockedList.get(0)).thenReturn("one");
  3. 通过Mockito.verify().doSomeThing(matchParam)方法来验证方法的调用情况(比如说调用次数,调用参数等),如Mockito.verify(mockedList).get(0);就是验证mockList对象是否调用了get(0)方法

对Mockito存在的误解

  1. Mockito.mock()并不是mock一整个类,而是根据传进去的一个类,mock出属于这个类的一个对象,并且返回这个mock对象;而传进去的这个类本身并没有改变,用这个类new出来的对象也没有受到任何改变;
  2. Mockito.verify()的参数必须是mock对象,否则会抛出异常“org.mockito.exceptions.misusing.NotAMockException:Argument passed to verify() is of type UserManager and is not a mock!”。也就是说,Mockito只能验证mock对象的调用情况;
  3. Mockito.mock()出来的对象并不会自动替换掉正式代码里面的对象,你必须要有某种方式(依赖注入方式:构造方法注入,set方式注入,或者是参数形式注入)把mock对象应用到正式代码里面;
  4. Mockito.spy()方法默认会调用这个类的real implementation,并返回相应的返回值,也可以通过Mockito.when().thenReturn()来指定spy对象的方法的行为;

对了,对spy对象的方法定制需要使用另一种方式:

    @Test
    public void testSpy() {
        List list = new LinkedList();
        List spy = Mockito.spy(list);

        //Impossible: real method is called so spy.get(0) throwsIndexOutOfBoundsException (the list is yet empty)
        when(spy.get(0)).thenReturn("foo");

        //You have to use doReturn() for stubbing
        doReturn("foo").when(spy).get(0);
    }

实验发现,when(spy.get(0)).thenReturn("foo");这行测试代码是测试不通过的,会抛出数组越界的异常来,

java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

    at java.util.LinkedList.checkElementIndex(LinkedList.java:555)
    at java.util.LinkedList.get(LinkedList.java:476)
    at com.chriszou.auttutorial.test.what.ListTest.testSpy(ListTest.java:39)

Process finished with exit code 255

因为用when(spy.get(0))会导致类LinkedList的spy对象的get()方法被真正执行,这一点需要时刻注意,所以就需要另一种写法。但是通过Mockito.mock()方法mock出来的对象,如果不指定的话,一个mock对象的所有非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等;而void方法将什么都不做。如:

// 创建mock对象
List mockedList = Mockito.mock(List.class);
System.out.println(mockedList.get(100));

我们未指定mockedList.get(100)的返回值,这里返回的就是null。

Mockito进阶

上篇博客的JUnit4单元测试例子中,我们讲到可以通过@Before、@Test、@After等注解来表示测试方法。在Mockito中,同样支持对变量进行注解,例如将mock对象设为测试类的属性,然后通过注解的方式@Mock来定义它,这样有利于减少重复代码,增强可读性,易于排查错误等。除了支持@Mock,Mockito支持的注解还有@Spy(监视真实的对象),@Captor(参数捕获器),@InjectMocks(mock对象自动注入)。

Annotation的初始化

只有Annotation还不够,要让它们工作起来还需要进行初始化工作。初始化的方法为:MockitoAnnotations.initMocks(testClass)参数testClass是你所写的测试类。一般情况下在Junit4的@Before定义的方法中执行初始化工作,如下:

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}

除了上述初始化的方法外,还可以使用Mockito提供的Junit Runner:MockitoJUnitRunner,这样就省略了上面的步骤。

@RunWith(MockitoJUnit44Runner.class)
public class ComplaintPresenterTest {
    ...
}
@Mock注解

使用@Mock注解来定义mock对象有如下的优点:

  1. 方便mock对象的创建
  2. 减少mock对象创建的重复代码
  3. 提高测试代码可读性
  4. 变量名字作为mock对象的标示,所以易于排错

下面是一个例子:

public class ComplaintPresenterTest {

    @Mock
    private ComplaintManager complaintManager;

    @Mock
    private ComplaintContract.IView iView;

    private ComplaintPresenter complaintPresenter;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        complaintPresenter = new ComplaintPresenter(complaintManager, iView);
    }

    @Test
    public void getComplaintReasons() throws Exception {
        complaintPresenter.getComplaintReasons();
        Mockito.verify(complaintManager, Mockito.times(1)).getComplaintReasons(any(Callback.class));
    }

}
@Spy注解

使用@Spy生成的类,所有方法都是真实方法,返回值和真实方法一样的,是使用Mockito.spy()的快捷方式

public class Test {  
    @Spy   
    List list = new LinkedList();
  
    @Before  
    public void init(){  
       MockitoAnnotations.initMocks(this);  
    }  
    ...  
}  
@Captor注解

@Captor是参数捕获器的注解,通过注解的方式可以更便捷的对ArgumentCaptor进行定义。还可以通过ArgumentCaptor对象的forClass(Class<T> clazz)方法来构建ArgumentCaptor对象,然后便可在验证时对方法的参数进行捕获,最后验证捕获的参数值。如果方法有多个参数都要捕获验证,那就需要创建多个ArgumentCaptor对象处理。

ArgumentCaptor的Api
argument.capture() 捕获方法参数;
argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值;
argument.getAllValues() 方法进行多次调用后,返回多个参数值;

下面看一个例子:

public class ComplaintPresenterTest {

    @Mock(name = "complaintManager1")
    private ComplaintManager complaintManager;

    @Mock
    private ComplaintContract.IView iView;

    private ComplaintPresenter complaintPresenter;

    @Captor
    private ArgumentCaptor<Callback<OrderConfig>> captor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        complaintPresenter = new ComplaintPresenter(complaintManager, iView);
    }

    @Test
    public void getComplaintReasons() throws Exception {
        complaintPresenter.getComplaintReasons();
        Mockito.verify(complaintManager).getComplaintReasons(captor.capture());
        Assert.assertNotNull(captor.getValue());
    }

}

上面例子就是验证complaintManager是否调用了getComplaintReasons方法,是否传入Callback<OrderConfig>参数,通过ArgumentCaptor可以对异步方法进行测试。可以参考这篇博客,通过ArgumentCaptor和doAnswer方式来实现对异步方法进行测试。

@InjectMocks注解

通过这个注解,可实现自动注入mock对象。当前版本只支持setter的方式进行注入,Mockito首先尝试类型注入,如果有多个类型相同的mock对象,那么它会根据名称进行注入。当注入失败的时候Mockito不会抛出任何异常,所以你可能需要手动去验证它的安全性。如下:

public class ComplaintPresenterTest {

    @Mock
    private ComplaintManager complaintManager;

    @Mock
    private ComplaintContract.IView iView;

    @Captor
    private ArgumentCaptor<Callback<OrderConfig>> captor;

    @InjectMocks
    private ComplaintPresenter complaintPresenter;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void getComplaintReasons() throws Exception {
        complaintPresenter.getComplaintReasons();
        Mockito.verify(complaintManager).getComplaintReasons(captor.capture());
        Assert.assertNotNull(captor.getValue());
    }

}
any参数匹配

很多时候你并不关心被调用方法的参数具体是什么,或者是你也不知道,你只关心这个方法得到调用了就行。这种情况下,Mockito提供了一系列的any方法,来表示任何的参数都行,如上面的例子:Mockito.verify(complaintManager, Mockito.times(1)).getComplaintReasons(any(Callback.class));
any(Callback.class)表示任何一个Callback对象都可以。null?也可以的!类似any,还有anyInt, anyLong, anyDouble等。anyObject表示任何对象,any(clazz)表示任何属于clazz的对象。在写这篇文章的时候,我刚刚发现,还有非常有意思也非常人性化的anyCollection,anyCollectionOf(clazz), anyList(Map, set), anyListOf(clazz)等。

Mockito高级进阶

在上面的例子中,

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

如果按照一般代码的思路去理解,是要做这么一件事:调用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使用的是方法调用,对方法的改名,可以用编译器支持的重构来进行,更加方便可靠。

Mock对象这件事情,本质上是一个Proxy模式的应用。Proxy模式说的是,在一个真实对象前面,提供一个proxy对象,所有对真实对象的调用,都先经过proxy对象,然后由proxy对象根据情况,决定相应的处理,它可以直接做一个自己的处理,也可以再调用真实对象对应的方法。Proxy对象对调用者来说,可以是透明的,也可以是不透明的。在阅读源码之前,可以先了解一下CGLIB,CGLIB是一个强大的高性能的代码生成包,被许多AOP的框架所使用,Mockito也使用了这个库。众所周知,JDK的动态代理用起来非常简单,当它有一个限制,就是使用动态代理的对象必须实现一个或多个接口。那么如果想代理没有实现接口的类,怎么办呢?对的,可以通过CGLIB来实现,它就是这么强大,可以代理没有实现接口的继承的类。

Mockito局限性

正是由于Mockito生成mock对象的原理是基于CGLIB,而CGLIB生成代理对象有其局限性,如final类型、private类型以及静态类型的方法不能mock。但是在我们项目中,如果要对静态方法或者final方法进行单元测试,那该怎么办呢?请关注博客Android单元测试之PowerMockito

小结

这篇博客主要介绍了mock的概念以及Mockito的使用,还有介绍了Mockito相关的注解使用,并简单介绍了Mockito的实现原理。

推荐阅读更多精彩内容

  • 背景 在写单元测试的过程中,一个很普遍的问题是,要测试的目标类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖...
    johnnycmj阅读 486评论 0 1
  • 写在前面 因个人能力有限,可能会出现理解错误的地方,欢迎指正和交流! 关于单元测试 通常一个优秀的开源框架,一般都...
    汪海游龙阅读 1,915评论 0 21
  • 前面花了很大篇幅来介绍JUnit4,JUnit4是整个单元测试的基础,其他的测试框架都是跑在JUnit4上的。接下...
    云飞扬1阅读 2,557评论 2 50
  • 什么是 Mock mock 的中文译为: 仿制的,模拟的,虚假的。对于测试框架来说,即构造出一个模拟/虚假的对象,...
    Whyn阅读 2,384评论 0 3
  • 本文介绍了Android单元测试入门所需了解的内容,包括JUnit、Mockito和PowerMock的使用,怎样...
    于卫国阅读 2,574评论 0 5