Android单元测试(六):Mockito学习

前面花了很大篇幅来介绍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单元测试(八):怎样测试异步代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345