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

简介: 前面花了很大篇幅来介绍JUnit4,JUnit4是整个单元测试的基础,其他的测试框架都是跑在JUnit4上的。接下来我们将来学习怎么样在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?

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

img_c5569d7fc02d18bc5e606333e102d31e.png

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


img_2edbe1df1570cdee0c1775b1bb34d2f4.png

如上图所示,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单元测试(八):怎样测试异步代码

目录
相关文章
|
28天前
|
安全 Java 数据库
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
这篇文章是关于Apache Shiro权限管理框架的详细学习指南,涵盖了Shiro的基本概念、认证与授权流程,并通过Spring Boot测试模块演示了Shiro在单应用环境下的使用,包括与IniRealm、JdbcRealm的集成以及自定义Realm的实现。
38 3
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
|
27天前
|
自然语言处理 机器人 Python
ChatGPT使用学习:ChatPaper安装到测试详细教程(一文包会)
ChatPaper是一个基于文本生成技术的智能研究论文工具,能够根据用户输入进行智能回复和互动。它支持快速下载、阅读论文,并通过分析论文的关键信息帮助用户判断是否需要深入了解。用户可以通过命令行或网页界面操作,进行论文搜索、下载、总结等。
42 1
ChatGPT使用学习:ChatPaper安装到测试详细教程(一文包会)
|
9天前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
24 2
|
10天前
|
编解码 安全 Linux
网络空间安全之一个WH的超前沿全栈技术深入学习之路(10-2):保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali——Liinux-Debian:就怕你学成黑客啦!)作者——LJS
保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali以及常见的报错及对应解决方案、常用Kali功能简便化以及详解如何具体实现
|
1月前
|
测试技术 开发者
vertx的学习总结6之动态代理类和测试
本文是Vert.x学习系列的第六部分,介绍了如何使用动态代理在事件总线上公开服务,以及如何进行Vert.x组件的异步测试,包括动态代理的创建和使用,以及JUnit 5和Vert.x测试工具的结合使用。
19 3
vertx的学习总结6之动态代理类和测试
|
1月前
|
测试技术
软件质量保护与测试(第2版)学习总结第十三章 集成测试
本文是《软件质量保护与测试》(第2版)第十三章的学习总结,介绍了集成测试的概念、主要任务、测试层次与原则,以及集成测试的不同策略,包括非渐增式集成和渐增式集成(自顶向下和自底向上),并通过图示详细解释了集成测试的过程。
52 1
软件质量保护与测试(第2版)学习总结第十三章 集成测试
|
1月前
|
测试技术
软件质量保护与测试(第2版)学习总结第十章 黑盒测试
本文是《软件质量保护与测试》(第2版)第十章的学习总结,介绍了黑盒测试的基本概念和方法,包括等价类划分、边界值分析和因果图法,并通过具体例子展示了如何设计测试用例来验证软件的功能性需求。
64 1
软件质量保护与测试(第2版)学习总结第十章 黑盒测试
|
1月前
|
人工智能 人机交互 数据库
软件质量保护与测试(第2版)学习总结第一章
本文是《软件质量保护与测试》(第2版)第一章的学习总结,概述了软件的特征、分类、软件工程的层次化技术、现代软件开发的变化,以及软件质量的概念和评价体系,包括黑盒、白盒和灰盒测试方法。
31 1
软件质量保护与测试(第2版)学习总结第一章
|
27天前
|
分布式计算 Hadoop 大数据
大数据体系知识学习(一):PySpark和Hadoop环境的搭建与测试
这篇文章是关于大数据体系知识学习的,主要介绍了Apache Spark的基本概念、特点、组件,以及如何安装配置Java、PySpark和Hadoop环境。文章还提供了详细的安装步骤和测试代码,帮助读者搭建和测试大数据环境。
49 1
|
27天前
|
测试技术 Python
自动化测试项目学习笔记(二):学习各种setup、tearDown、断言方法
本文主要介绍了自动化测试中setup、teardown、断言方法的使用,以及unittest框架中setUp、tearDown、setUpClass和tearDownClass的区别和应用。
49 0
自动化测试项目学习笔记(二):学习各种setup、tearDown、断言方法