Android 单元测试 Mockito使用详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mq2553299/article/details/77014651

简介

笔者的Android单元测试相关系列:

Android单元测试:Mockito使用详解

Android单元测试:使用本地数据测试Retrofit

Android单元测试:测试RxJava的同步及异步操作

Android 自动化测试 Espresso篇:简介&基础使用

Android 自动化测试 Espresso篇:异步代码测试

什么是mock测试,什么是mock对象?

先来看看下面这个示例:

这里写图片描述

从上图可以看出如果我们要对A进行测试,那么就要先把整个依赖树构建出来,也就是BCDE的实例。

一种替代方案就是使用mocks

这里写图片描述
从图中可以清晰的看出

mock对象就是在调试期间用来作为真实对象的替代品。

mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。

用四个字简单概括,就是「依赖隔离」。

Mockito简介

Mockito是一个流行的Mocking(模拟测试)框架,通过使用Mocking框架,可以尽可能使unit test独立的。unit test保持独立的好处不在这里讨论。
官方文档: http://docs.mockito.googlecode.com/hg/latest/org/mockito/Mockito.html

我们先来看如何添加Mockito的依赖:

这里写图片描述

//mockito
testCompile "org.mockito:mockito-core:2.8.9"
androidTestCompile "org.mockito:mockito-android:2.8.9"

ok,接下来我们就来看看怎样使用Mockito的API吧。

在Test代码中使用Mockito

初始化注入

首先我们在setUp函数中进行初始化:

private ArrayList mockList;

@Before
public void setUp() throws Exception {
    //MockitoAnnotations.initMocks(this);
    //mock creation
    mockList = mock(ArrayList.class);
}

当然,你也这样这样进行注入:

@Mock
private ArrayList mockList;

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

initMocks(this)后,就可以通过@Mock注解直接使用mock对象。

简单的例子

    @Test
    public void sampleTest1() throws Exception {
        //使用mock对象执行方法
        mockList.add("one");
        mockList.clear();

        //检验方法是否调用
        verify(mockList).add("one");   
        verify(mockList).clear();     
    }

我们可以看到,我们可以直接调用mock对象的方法,比如ArrayList.add()或者ArrayList.clear(),然后我们通过verify函数进行校验。

直接mock接口对象

正常来讲我们想要一个接口类型的对象,首先我们需要先实例化一个对象并实现,其对应的抽象方法,但是有了mock,我们可以直接mock出一个接口对象:

@Test
public void sampleTest2() throws Exception {
    //我们可以直接mock一个借口,即使我们并未声明它
    MVPContract.Presenter mockPresenter = mock(MVPContract.Presenter.class);
    when(mockPresenter.getUserName()).thenReturn("qingmei2"); //我们定义,当mockPresenter调用getUserName()时,返回qingmei2
    String userName = mockPresenter.getUserName();

    verify(mockPresenter).getUserName(); //校验 是否mockPresenter调用了getUserName()方法
    Assert.assertEquals("qingmei2", userName); //断言 userName为qingmei2

//        verify(mockPresenter).getPassword();  //校验 是否mockPresenter调用了getPassword()方法
    String password = mockPresenter.getPassword();  //因为未定义返回值,默认返回null
    verify(mockPresenter).getPassword();
    Assert.assertEquals(password, null);
}

参数匹配器

@Test
public void argumentMatchersTest3() throws Exception {
    when(mockList.get(anyInt())).thenReturn("不管请求第几个参数 我都返回这句");
    System.out.println(mockList.get(0));
    System.out.println(mockList.get(39));

    //当mockList调用addAll()方法时,「匹配器」如果传入的参数list size==2,返回true;
    when(mockList.addAll(argThat(getListMatcher()))).thenReturn(true);

    //根据API文档,我们也可以使用lambda表达式: 「匹配器」如果传入的参数list size==3,返回true;
//        when(mockList.addAll(argThat(list -> list.size() == 3))).thenReturn(true);
    //我们不要使用太严格的参数Matcher,也许下面会更好
//        when(mockList.addAll(argThat(notNull()));

    boolean b1 = mockList.addAll(Arrays.asList("one", "two"));
    boolean b2 = mockList.addAll(Arrays.asList("one", "two", "three"));

    verify(mockList).addAll(argThat(getListMatcher()));
    Assert.assertTrue(b1);
    Assert.assertTrue(!b2);
}

private ListOfTwoElements getListMatcher() {
    return new ListOfTwoElements();
}

/**
 * 匹配器,用来测试list是否有且仅存在两个元素
 */
class ListOfTwoElements implements ArgumentMatcher<List> {
    public boolean matches(List list) {
        return list.size() == 2;
    }

    public String toString() {
        //printed in verification errors
        return "[list of 2 elements]";
    }
}

对于一个Mock的对象,有时我们需要进行校验,但是基础的API并不能满足我们校验的需要,我们可以自定义Matcher,比如案例中,我们自定义一个Matcher,只有容器中两个元素时,才会校验通过。

验证方法的调用次数

 /**
   * 我们也可以测试方法调用的次数
   * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#exact_verification
   *
   * @throws Exception
   */
@Test
public void simpleTest4() throws Exception {
    mockList.add("once");

    mockList.add("twice");
    mockList.add("twice");

    mockList.add("three times");
    mockList.add("three times");
    mockList.add("three times");

    verify(mockList).add("once");  //验证mockList.add("once")调用了一次 - times(1) is used by default
    verify(mockList, times(1)).add("once");//验证mockList.add("once")调用了一次

    //调用多次校验
    verify(mockList, times(2)).add("twice");
    verify(mockList, times(3)).add("three times");

    //从未调用校验
    verify(mockList, never()).add("four times");

    //至少、至多调用校验
    verify(mockList, atLeastOnce()).add("three times");
    verify(mockList, atMost(5)).add("three times");
//        verify(mockList, atLeast(2)).add("five times"); //这行代码不会通过
}

抛出你想要的异常

/**
  * 异常抛出测试
  * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#stubbing_with_exceptions
  */
@Test
public void throwTest5() {
    doThrow(new NullPointerException("throwTest5.抛出空指针异常")).when(mockList).clear();
    doThrow(new IllegalArgumentException("你的参数似乎有点问题")).when(mockList).add(anyInt());

    mockList.add("string");//这个不会抛出异常
    mockList.add(12);//抛出了异常,因为参数是Int
    mockList.clear();
}

如案例所示,当mockList对象执行clear方法时,抛出空指针异常,当其执行add方法,且传入的参数类型为int时,抛出非法参数异常。

校验方法执行顺序

/**
  * 验证执行执行顺序
  * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#in_order_verification
  *
  * @throws Exception
  */
@Test
public void orderTest6() throws Exception {
    List singleMock = mock(List.class);

    singleMock.add("first add");
    singleMock.add("second add");

    InOrder inOrder = inOrder(singleMock);

    //inOrder保证了方法的顺序执行
    inOrder.verify(singleMock).add("first add");
    inOrder.verify(singleMock).add("second add");

    List firstMock = mock(List.class);
    List secondMock = mock(List.class);

    firstMock.add("first add");
    secondMock.add("second add");

    InOrder inOrder1 = inOrder(firstMock, secondMock);

    //下列代码会确认是否firstmock优先secondMock执行add方法
    inOrder1.verify(firstMock).add("first add");
    inOrder1.verify(secondMock).add("second add");
}

有时候我们需要校验方法执行顺序的先后,如案例所示,inOrder对象会判断方法执行顺序,如果顺序不对,该测试案例failed。

确保mock对象从未进行过交互

/**
  * 确保mock对象从未进行过交互
  * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#never_verification
  *
  * @throws Exception
  */
@Test
public void noInteractedTest7() throws Exception {
    List firstMock = mock(List.class);
    List secondMock = mock(List.class);
    List thirdMock = mock(List.class);

    firstMock.add("one");

    verify(firstMock).add("one");

    verify(firstMock, never()).add("two");

    firstMock.add(thirdMock);
    // 确保交互(interaction)操作不会执行在mock对象上
//        verifyZeroInteractions(firstMock); //test failed,因为firstMock和其他mock对象有交互
    verifyZeroInteractions(secondMock, thirdMock);   //test pass
}

可能是因为水平有限,笔者很少用到这个API(好吧除了学习案例中用过其他基本没怎么用过),不过还是敲一遍,保证有个基础的印象。

简化mock对象的创建

/**
  * 简化mock对象的创建,请注意,一旦使用@Mock注解,一定要在测试方法调用之前调用(比如@Before注解的setUp方法)
  * MockitoAnnotations.initMocks(testClass);
  */
@Mock
List mockedList;
@Mock
User mockedUser;

@Test
public void initMockTest8() throws Exception {
    mockedList.add("123");
    mockedUser.setLogin("qingmei2");
}

注释写的很明白了,不赘述

方法连续调用测试

/**
  * 方法连续调用的测试
  * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#stubbing_consecutive_calls
  */
@Test
public void continueMethodTest9() throws Exception {
    when(mockedUser.getName())
            .thenReturn("qingmei2")
            .thenThrow(new RuntimeException("方法调用第二次抛出异常"))
            .thenReturn("qingemi2 第三次调用");

    //另外一种方式
    when(mockedUser.getName()).thenReturn("qingmei2 1", "qingmei2 2", "qingmei2 3");

    String name1 = mockedUser.getName();

    try {
        String name2 = mockedUser.getName();
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }

    String name3 = mockedUser.getName();

    System.out.println(name1);
    System.out.println(name3);
}

有用,但不重要,学习一下加深印象。

为回调方法做测试

/**
  * 为回调方法做测试
  * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#answer_stubs
  */
@Test
public void callBackTest() throws Exception {

    when(mockList.add(anyString())).thenAnswer(new Answer<Boolean>() {

        @Override
        public Boolean answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            Object mock = invocation.getMock();
            return false;
        }
    });
    System.out.println(mockList.add("第1次返回false"));

    //lambda表达式
    when(mockList.add(anyString())).then(invocation -> true);
    System.out.println(mockList.add("第2次返回true"));

    when(mockList.add(anyString())).thenReturn(false);
    System.out.println(mockList.add("第3次返回false"));
}

在Mockito的官方文档中,这样写道:

在最初的Mockito里也没有这个具有争议性的特性。我们建议使用thenReturn() 或thenThrow()来打桩。这两种方法足够用于测试或者测试驱动开发。

实际上笔者日常开发中也不怎么用到这个特性。

拦截方法返回值(常用)

/**
  * doReturn()、doThrow()、doAnswer()、doNothing()、doCallRealMethod()系列方法的运用
  * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#do_family_methods_stubs
  */
@Test
public void returnTest() throws Exception {
    //返回值为null的函数,可以通过这种方式进行测试

    doAnswer(invocation -> {
        System.out.println("测试无返回值的函数");
        return null;
    }).when(mockList).clear();

    doThrow(new RuntimeException("测试无返回值的函数->抛出异常"))
            .when(mockList).add(eq(1), anyString());

    doNothing().when(mockList).add(eq(2), anyString());

//        doReturn("123456").when(mockList).add(eq(3), anyString());    //不能把空返回值的函数与doReturn关联

    mockList.clear();
    mockList.add(2, "123");
    mockList.add(3, "123");
    mockList.add(4, "123");
    mockList.add(5, "123");

    //但是请记住这些add实际上什么都没有做,mock对象中仍然什么都没有
    System.out.print(mockList.get(4));
}

我们不禁这样想,这些方法和when(mock.do()).thenReturn(foo)这样的方法有什么区别,或者说,这些方法有必要吗?

答案是肯定的,因为在接下来介绍的新特性Spy中,该方法起到了至关重要的作用。

可以说,以上方法绝对是不可代替的。

Spy:监控真实对象(重要)

/**
  * 监控真实对象
  * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#spy
  */
@Test
public void spyTest() throws Exception {
   List list = new ArrayList();
   List spyList = spy(list);

   //当spyList调用size()方法时,return100
   when(spyList.size()).thenReturn(100);

   spyList.add("one");
   spyList.add("two");

   System.out.println("spyList第一个元素" + spyList.get(0));
   System.out.println("spyList.size = " + spyList.size());

   verify(spyList).add("one");
   verify(spyList).add("two");

   //请注意!下面这行代码会报错! java.lang.IndexOutOfBoundsException: Index: 10, Size: 2
   //不可能 : 因为当调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生异常,因为真实List对象是空的
//        when(spyList.get(10)).thenReturn("ten");

   //应该这么使用

   doReturn("ten").when(spyList).get(9);
   doReturn("eleven").when(spyList).get(10);

   System.out.println("spyList第10个元素" + spyList.get(9));
   System.out.println("spyList第11个元素" + spyList.get(10));

   //Mockito并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互
   //不要期望从监控对象得到正确的结果。当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。

   //因此结论就是 : 当你在监控一个真实对象时,你想在stub这个真实对象的函数,那么就是在自找麻烦。或者你根本不应该验证这些函数。
}

Spy绝对是一个好用的功能,我们不要滥用,但是需要用到对真实对象的测试操作,spy绝对是一个很不错的选择。

捕获参数(重要)

/**
 * 为接下来的断言捕获参数(API1.8+)
 * https://static.javadoc.io/org.mockito/mockito-core/2.8.9/org/mockito/Mockito.html#captors
 */
@Test
public void captorTest() throws Exception {
    Student student = new Student();
    student.setName("qingmei2");

    ArgumentCaptor<Student> captor = ArgumentCaptor.forClass(Student.class);
    mockList.add(student);
    verify(mockList).add(captor.capture());

    Student value = captor.getValue();

    Assert.assertEquals(value.getName(),"qingmei2");
}

@Data
private class Student {
    private String name;
}

我们将定义好的ArgumentCaptor参数捕获器放到我们需要去监控捕获的地方,如果真的执行了该方法,我们就能通过captor.getValue()中取到参数对象,如果没有执行该方法,那么取到的只能是null或者基本类型的默认值。

小结

本文看起来是枯燥无味的,事实上也确实如此,但是如果想在开发中写出高覆盖率的单元测试,Mockito强大的功能一定能让你学会之后爱不释手。

在接下来的文章中,笔者会通过实际案例,阐述自己在实际的Android APP开发过程中,MVP+Rxjava+Retrofit模式下如何进行单元测试的编写。

参考文档

Mocktio 2.8.9 API 官方文档

Mocktio 2.8.9 API 中文文档

案例代码:

本文所有案例代码,点我查看

猜你喜欢

转载自blog.csdn.net/mq2553299/article/details/77014651