本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《 阿里云开发者社区用户服务协议 》和 《 阿里云开发者社区知识产权保护指引 》。如果您发现本社区中有涉嫌抄袭的内容,填写 侵权投诉表单 进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

前面花了很大篇幅来介绍JUnit4,JUnit4是整个单元测试的基础,其他的测试框架都是跑在JUnit4上的。接下来我们将来学习怎么样在Android的单元测试中集成Mockito。

6.1 Mockito介绍

6.1.1 Mockito是什么?

Mockito是一个用于java单元测试中的mocking框架,mock就是模拟的意思,就是能够模拟一些类和方法的实现。
其官网地址: http://site.mockito.org

6.1.2 为什么需要mock?

在写单元测试的时候,我们会遇到某个测试类有很多依赖,这些依赖类或对象又有别的依赖,这样会形成一棵巨大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是及其困难的,有时候甚至因为运行环境的关系,几乎不可能完整地构建出这些依赖。如下图所示:


如果我们要针对ClassA来写单元测试,发现ClassA依赖ClassB和ClassD,ClassB又依赖ClassC和ClassE,ClassE又依赖ClassF。好吧,写到这里我自己都头疼了,我仅仅是想为ClassA写一个单元测试而已,却不得不自己构造这么多依赖对象,太复杂了。
由上图可以看到,ClassA只依赖ClassB和ClassD,我们实际上只需要构造这2个依赖对象,这2个依赖对象分别实现了ClassB和ClassD的所有功能。ClassA并不关心ClassB的依赖对象ClassC和ClassE是怎么构造,它只关心ClassB和ClassD的构造,并且也无需关系他们的实现细节,有没什么方法能自动帮我们实现呢,那这样我们编写单元测试就容易得多了,Mockito框架就是为了解决这个问题而设计的。


如上图所示,Mockito框架自动帮我们构造了2个mock对象:MockClassB和MockClassD,这样ClassA的单元测试就简单多了。
Mock测试就是在测试过程中,对于一些由于运行环境原因不能构造的对象、或者构造比较复杂的对象、或者我们并不需要关注的对象,用一个虚拟的对象(Mock对象)来替代从而方便测试的测试方法。

6.1.3 在Android中使用Mockito

在build.gradle中加入mockito依赖配置:

    testCompile 'org.mockito:mockito-core:2.8.9'

最新版本可以去官网查看

6.2 使用Mockito

几乎所有的测试方法都在org.mockito.Mockito类中:

6.2.1 验证行为
    @Test
    public void testMock() {
        //创建一个mock对象
        List list = mock(List.class);
        //使用mock对象
        list.add("one");
        list.clear();
        //验证mock对象的行为
        verify(list).add("one");  //验证有add("one")行为发生
        verify(list).clear();          //验证有clear()行为发生

一旦创建一个mock对象,它会记住所有的交互,这样我们就可以验证自己感兴趣的行为。

6.2.2 Stubbing

Stub对象用来提供测试时所需要的测试数据,对各种交互设置相应的回应。Mockito使用when(...).thenReturn(...)设置方法调用的返回值,使用when(...).thenThrow(...)设置方法调用时抛出的异常。

    @Test
    public void testMock2() {
        //不仅可以针对接口mock, 还可以针对具体类
        LinkedList list = mock(LinkedList.class);
        //设置返回值,当调用list.get(0)时会返回"first"
        when(list.get(0)).thenReturn("first");
        //当调用list.get(1)时会抛出异常
        when(list.get(1)).thenThrow(new RuntimeException());
        //会打印"print"
        System.out.println(list.get(0));
        //会抛出RuntimeException
        System.out.println(list.get(1));
        //会打印 null
        System.out.println(list.get(99));
        verify(list).get(0);

对于stubbing,需要注意一下几点:

  • 对于有返回值的方法,mock会默认返回null、空集合、默认值。比如为int/Integer返回0,为boolean/Boolean返回false、为Object返回null。
  • 一旦stubbing,不管方法被调用多少次,都永远返回stubbing的值。
  • stubbing可以被覆盖, 如果对同一个方法进行多次stubbing,最后一次的stubbing会生效。
  • 6.2.3 Argument matchers(参数匹配器)
        @Test
        public void testMock3() {
            List list = mock(List.class);
            //使用anyInt(), anyString(), anyLong()等进行参数匹配
            when(list.get(anyInt())).thenReturn("item");
            //将会打印出"item"
            System.out.println(list.get(100));
            verify(list).get(anyInt());
    
    6.2.4 验证方法的调用次数
        @Test
        public void testMock4() {
            List list = mock(List.class);
            list.add("once");
            list.add("twice");
            list.add("twice");
            list.add("triple");
            list.add("triple");
            list.add("triple");
            //执行1次
            verify(list, times(1)).add("once");
            //执行2次
            verify(list, times(2)).add("twice");
            verify(list, times(3)).add("triple");
            //从不执行, never()等同于times(0)
            verify(list, never()).add("never happened");
            //验证至少执行1次
            verify(list, atLeastOnce()).add("twice");
            //验证至少执行2次
            verify(list, atLeast(2)).add("twice");
            //验证最多执行4次
            verify(list, atMost(4)).add("triple");
    

    times(n):方法被调用n次
    never():没有被调用
    atLeast(n):至少被调用n次
    atLeastOnce():至少被调用1次,相当于atLeast(1)
    atMost():最多被调用n次

    6.2.5 验证方法的调用顺序
        @Test
        public void testMock5() {
            List list = mock(List.class);
            list.add("first");
            list.add("second");
            InOrder myOrder = inOrder(list);
            myOrder.verify(list).add("first");
            myOrder.verify(list).add("second");
    

    可同时验证多个mock对象的测试方法的执行顺序:
    InOrder myOrder = inOrder(firstMock, secondMock, ...)

    6.2.6 verifyZeroInteractions && verifyNoMoreInteractions
        @Test
        public void testMock6() {
            List list = mock(List.class);
            //验证mock对象没有产生任何交互,也即没有任何方法调用
            verifyZeroInteractions(list);
            List list2 = mock(List.class);
            list2.add("one");
            list2.add("two");
            verify(list2).add("one");
            //验证mock对象是否有被调用过但没被验证的方法。这里会测试不通过,list2.add("two")方法没有被验证过
            verifyNoMoreInteractions(list2);
    
    6.2.7 使用@Mock创建mock对象
        //通过注解会自动创建mock对象
        @Mock
        private List mockList;
        @Mock
        private Map mockMap;
    

    要使用@Mock注解有2种配置方式:

  • 在base class中或者初始化的地方配置:
  • MockitoAnnotations.initMocks(this);
    
  • 使用JUnit4的rule来配置:
  •     @Rule
        public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
    

    以上2种方式可以达到同样的效果。

    6.2.8 do(...).when(...)
  • doThrow(Throwable...):进行异常测试
  •     @Test
        public void testMock7() {
            List list = mock(List.class);
            list.add("123");
            //当list调用clear()方法时会抛出异常
            doThrow(new RuntimeException()).when(list).clear();
            list.clear();
    
  • doReturn():指定返回值
  •     @Test
        public void testMock8() {
            List list = mock(List.class);
            doReturn("123").when(list).get(anyInt());
            System.out.println(list.get(0));
    
  • doNothing() :指定void方法什么都不做
  • doCallRealMethod():指定方法调用内部的真实逻辑
  •     class Foo {  
            public void doFoo() {
                System.out.println("method doFoo called.");
            public int getCount() {
                return 1;
        @Test
        public void testMock9() {
            Foo foo = mock(Foo.class);
            //什么信息也不会打印, mock对象并不会调用真实逻辑
            foo.doFoo();
            //啥也不会打印出来
            doNothing().when(foo).doFoo();
            foo.doFoo();
            doCallRealMethod().when(foo).doFoo();
            //这里会调用真实逻辑, 打印出"method doFoo called."信息
            foo.doFoo();
            //这里会打印出0
            System.out.println(foo.getCount());
            doCallRealMethod().when(foo).getCount();
            //这里会打印出"1"
            System.out.println(foo.getCount());
    
    6.2.9 使用spy()监视真正的对象

    使用spy可以监视对象方法的真实调用。当我们mock某个类时,如果需要某些方法是真实调用,而某些方法是mock调用时,借助spy可以实现这些功能。

        @Test
        public void testMock10(){
            List list = new ArrayList();
            List spy = spy(list);
            //subbing方法,size()并不会真实调用,这里返回10
            when(spy.size()).thenReturn(10);
            //使用spy对象会调用真实的方法
            spy.add("one");
            spy.add("two");
            //会打印出"one"
            System.out.println(spy.get(0));
            //会打印出"10",与前面的stubbing方法对应
            System.out.println(spy.size());
            //对spy对象依旧可以来验证其行为
            verify(spy).add("one");
            verify(spy).add("two");
    
    6.2.10 参数捕捉
        @Test
        public void testMock11() {
            List list = mock(List.class);
            ArgumentCaptor<String> args = ArgumentCaptor.forClass(String.class);
            list.add("one");
            //验证后再捕捉参数
            verify(list).add(args.capture());
            Assert.assertEquals("one", args.getValue());
    
    6.2.11 重置mocks
        @Test
        public void testMock12() {
            List list = mock(List.class);
            when(list.size()).thenReturn(100);
            //打印出"100"
            System.out.println(list.size());
            //充值mock, 之前的交互和stub将全部失效
            reset(list);
            //打印出"0"
            System.out.println(list.size());
    
    6.2.12 更多的注解

    使用注解都需要预先进行配置,怎么配置见6.2.7说明

  • @Captor 替代ArgumentCaptor
  • @Spy 替代spy(Object)
  • @Mock 替代mock(Class)
  • @InjectMocks 创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中
  • 6.3 容易概念混淆的几个点

    6.3.1 @Mock与@Spy的异同
  • Mock对象只能调用stubbed方法,不能调用其真实的方法。而Spy对象可以监视一个真实的对象,对Spy对象进行方法调用时,会调用真实的方法。
  • 两者都可以stubbing对象的方法,让方法返回我们的期望值。
  • 两者无论是否是真实的方法调用,都可进行verify验证。
  • 对final类、匿名类、java的基本数据类型是无法进行mock或者spy的。
  • 注意mockito是不能mock static方法的。
  • 6.3.2 @InjectMocks与@Mock等的区别

    @Mock:创建一个mock对象。
    @InjectMocks:创建一个实例对象,然后将@Mcok或者@Spy注解创建的mock对象注入到该实例对象中。
    stackoverflow上对这个有一个比较形象的解释:
    https://stackoverflow.com/questions/16467685/difference-between-mock-and-injectmocks

    @RunWith(MockitoJUnitRunner.class)
    public class SomeManagerTest {
        @InjectMocks
        private SomeManager someManager;
        @Mock
        private SomeDependency someDependency; // 该mock对象会被注入到someManager对象中
        //你不用向下面这样实例化一个SomeManager对象,@InjectMocks会自动帮你实现
        //SomeManager someManager = new SomeManager();    
        //SomeManager someManager = new SomeManager(someDependency);
    
    6.3.3 when(...).thenReturn()与doReturn(...).when(...)两种语法的异同
  • 两者都是用来stubbing方法的,大部分情况下,两者可以表达同样的意思,与Java里的do/while、while/do语句类似。
  • 对void方法不能使用when/thenReturn语法。
  • 对spy对象要慎用when/thenReturn,如:
  •         List spyList = spy(new ArrayList());
            //下面代码会抛出IndexOutOfBoundsException
            when(spyList.get(0)).thenReturn("foo");
            //这里不会抛出异常
            doReturn("foo").when(spyList).get(0);
            System.out.println(spyList.get(0));
    

    这段代码运行会抛出异常,当调用when(spyList.get(0)).thenReturn("foo")时,会调用真实对象的get(0),由于list是空的所以会抛出IndexOutOfBoundsException异常。用doReturn/when语法则不会,因为它不会真实调用get(0)方法。
    个人觉得讨论哪种语法好是没有意义的,推荐使用doReturn/when语法,不管是mock还是spy对象都适用。

    6.4 小结

    本文主要介绍了mockito框架的使用方法,以及为什么要使用mockito来进行单元测试。熟练掌握mockito的常用方法,对我们来写单元测试来说绝对是事半功倍。

    系列文章:
    Android单元测试(一):前言
    Android单元测试(二):什么是单元测试
    Android单元测试(三):测试难点及方案选择
    Android单元测试(四):JUnit介绍
    Android单元测试(五):JUnit进阶
    Android单元测试(六):Mockito学习
    Android单元测试(七):Robolectric介绍
    Android单元测试(八):怎样测试异步代码

    Android体系课学习 之 网络请求库Retrofit源码分析-看这一篇就够了
    - 网络请求在我们开发中起的很大比重,有一个好的网络框架可以节省我们的开发工作量,也可以避免一些在开发中不该出现的bug - *Retrofit*是一个轻量级框架,基于*OkHttp*的一个*Restful*框架
    公众号:小余的自习室 Android体系课学习 之 网络请求库Retrofit使用方式(附Demo)
    - 网络请求在我们开发中起的很大比重,有一个好的网络框架可以节省我们的开发工作量,也可以避免一些在开发中不该出现的bug - Retrofit是一个轻量级框架,基于OkHttp的一个Restful框架
    公众号:小余的自习室