天天看点

Android 单元测试之Mockito框架的使用前序Mock的概念集成Mocktio四种Mock方式验证互动(Interactions)设置桩(Stub)验证模式(Verification Mode)参数匹配器(Argument Matcher)其他方法

前序

首先我们要理解mock的概念,然后学习使用mock来做单元测试。关于Mock的框架有很多,比如Mockito、PowerMock、EasyMock等等,本文主要介绍Mockito的用法,各种框架的对比不在本文阐述范围,而且此类框架大体相同,只需要学习其中一个就能轻松地学习其他框架,没必要纠结那个框架才是最好的

Mock的概念

首先要明白为什么要用Mock,什么是

Mock

,Mock能干什么这三个问题。

  • 为什么要用Mock

    在传统的JUnit单元测试中,我们没有消除在测试中对对象的依赖。如存在A对象方法依赖B对象方法,在测试A对象的时候,我们需要构造出B对象,这样子增加了测试的难度,或者使得我们对某些类的测试无法实现。这与单元测试的思路相违背。

  • Mock是什么

    Mock的中文意思是“模仿”,Mock就是去构造(模仿)一个虚拟的对象,而这个对象通常比较难直接创建。

  • Mock能干什么

    有了Mock可以轻松地帮助你对复杂的功能解耦,实现单元测试。比如下文的Log类,你会发现它依赖于Android运行环境,很难把整个依赖树都构建出来,所以我们需要Mock。

Android 单元测试之Mockito框架的使用前序Mock的概念集成Mocktio四种Mock方式验证互动(Interactions)设置桩(Stub)验证模式(Verification Mode)参数匹配器(Argument Matcher)其他方法

集成Mocktio

dependencies {
    //...
    testCompile "org.mockito:mockito-core:2.+"
}
           

四种Mock方式

  • 普通方法:
@Test
public void testIsNotNull(){
   Person mPerson = mock(Person.class); //<--使用mock方法

   assertNotNull(mPerson);
}
           
  • 注解方法:
@Mock
    Person mPerson;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);//<--初始化
    }

    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }
           
  • 运行器方法:
@RunWith(MockitoJUnitRunner.class) //<--使用MockitoJUnitRunner
public class MockitoJUnitRunnerTest {

    @Mock //<--使用@Mock注解
    Person mPerson;

    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }

}
           
  • MockitoRule方法
public class MockitoRuleTest {

    @Mock //<--使用@Mock注解
    Person mPerson;

    @Rule //<--使用@Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testIsNotNull(){
        assertNotNull(mPerson);
    }

}
           

验证互动(Interactions)

下面来使用Mockito验证互动功能,比如说验证TextView的setText方法交互情况

TextView mockedTextView;

    @Test
    public void test(){
        mockedTextView=mock(TextView.class);
        mockedTextView.setText("test");
        Mockito.verify(mockedTextView).setText("test");

        System.out.println(mockedTextView.getText());
    }
           
Android 单元测试之Mockito框架的使用前序Mock的概念集成Mocktio四种Mock方式验证互动(Interactions)设置桩(Stub)验证模式(Verification Mode)参数匹配器(Argument Matcher)其他方法
  • mock方法用于“模仿”一个对象并返回这个对象
  • verify方法则是用于验证“模仿对象”的互动。
  • 特别注意:如果你使用mockedTextView.getText()获取设置的值会发现返回值为null

设置桩(Stub)

  • Stub( 去伪造一个方法,阻断对原来方法的调用。如下伪造了一个mockedTextView.getText() 方法)

    模拟一个Object,当输入特定值的时候,返回hard code的指定值,并不真正执行逻辑,类似于复写(override)了该方法,在复写的方法中不执行任何逻辑只返回了特定值

上面最后说到

mockedTextView.getText()

会返回一个null,假设我们需要测试mockedTextView.getText()返回值是否正确怎么处理呢?Mockito给我们设置方法桩功能。简单来说就是“指定方法返回的结果”,比如下面代码:

TextView mockedTextView = Mockito.mock(TextView.class);
Mockito.when(mockedTextView.getText()).thenReturn("test");
System.out.println(mockedTextView.getText());
           
  • when方法指定要设置桩的方法
  • thenReturn来指定返回值
  • 所以当我们调用mockedTextView.getText() 方法时,返回 test
  • 设置桩的值可以设置多次,只会返回最后一次设置的值。(如下图)
    Android 单元测试之Mockito框架的使用前序Mock的概念集成Mocktio四种Mock方式验证互动(Interactions)设置桩(Stub)验证模式(Verification Mode)参数匹配器(Argument Matcher)其他方法

因为

mock

出的对象中的非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。“打桩”顾名思义就是将我们Mock出的对象进行操作,比如提供模拟的返回值等

常用打桩方法

  • 关心的是方法或属性的返回值
方法名 方法描述
thenReturn(T value) 设置要返回的值
thenThrow(Throwable… throwables) 设置要抛出的异常
thenAnswer(Answer answer) 对结果进行拦截
doReturn(Object toBeReturned) 提前设置要返回的值
doThrow(Throwable… toBeThrown) 提前设置要抛出的异常
doAnswer(Answer answer) 提前对结果进行拦截
doCallRealMethod() 调用某一个方法的真实实现
doNothing() 设置void方法什么也不做

抛出异常

若果需要某个方法抛出异常,可以使用下面的方法:

//void返回方法
Mockito.doThrow(new RuntimeException()).when(mockedTextView).setText("abc");

//非void返回方法
Mockito.when(mockedTextView.getText()).thenThrow(new RuntimeException());
           

其中注意区分不同返回类型的写法不同。另外如果需要防止异常中断执行,可以在增加一个doNothing方法,代码如下:(只有Void返回类型方法才能使用doNothing())

自定义应答(Answer)

 对于一个方法设置桩when…thenXxx或者doXxxx…when的组合外,Mockito给了一个自定义应答的的方法让我们自定义方法应答的内容。试想一下,假设有一个异步方法(当然返回类型就是Void)的回调中有多个回调,当你想指定执行某个回调之前学到的显然就不那么容易实现了。如果自定义Answer内容,那将是非常简单的,示例代码如下:

Mockito.doAnswer(new Answer() {
    @Override
    public Object answer(InvocationOnMock invocationOnMock) throws Throwable {

        //获取第一个参数
        Object callback = invocationOnMock.getArgument();

        //指定回调执行操作
        return callback.onFinished();
    }

}).when(mockedClass.asyncRequset(callback));//执行一步操作
           

或者举一个简单的例子(采用when…thenAnswer方式):

Mockito.when(mockedTextView.getText()).thenAnswer(new Answer<String>() {
     @Override
     public String answer(InvocationOnMock invocationOnMock) throws Throwable {
         System.out.println("custom answer");
         return "test";
      }
});
System.out.print(mockedTextView.getText());
           

很明显,这里最终输出为:

custom answer
test
           

验证模式(Verification Mode)

  • 关心的是方法在特定环境是否被调用,调用的次数
前面所说的都是状态测试,但是如果不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。从概念上讲,就是和状态测试所不同的“行为测试”了。

常用验证方法

方法名 方法描述
after(long millis) 在给定的时间后进行验证
timeout(long millis) 验证方法执行是否超时
atLeast(int minNumberOfInvocations) 至少进行n次验证(n为参数)
atMost(int maxNumberOfInvocations) 至多进行n次验证(n为参数)
description(String description) 验证失败时输出的内容
times(int wantedNumberOfInvocations) 验证调用方法的次数
never() 验证交互没有发生,相当于times(0)
only() 验证方法只被调用一次,相当于times(1)

例子

//延时s验证
System.out.println(mockedTextView.getText());
System.out.println(System.currentTimeMillis());
Mockito.verify(mockedTextView, after()).getText();
System.out.println(System.currentTimeMillis());

//最少执行一次验证
Mockito.verify(mockedTextView, atLeast()).getText();
           

参数匹配器(Argument Matcher)

  • 有时候我们不关心输入,而是关系输入的类型,以及调用该方法的次数,比如说setText()方法:
    Android 单元测试之Mockito框架的使用前序Mock的概念集成Mocktio四种Mock方式验证互动(Interactions)设置桩(Stub)验证模式(Verification Mode)参数匹配器(Argument Matcher)其他方法

常用参数匹配器

方法名 方法描述
anyObject() 匹配任何对象
any(Class type) 与anyObject()一样
any() 与anyObject()一样
anyBoolean() 匹配任何boolean和非空Boolean
anyByte() 匹配任何byte和非空Byte
anyCollection() 匹配任何非空Collection
anyDouble() 匹配任何double和非空Double
anyFloat() 匹配任何float和非空Float
anyInt() 匹配任何int和非空Integer
anyList() 匹配任何非空List
anyLong() 匹配任何long和非空Long
anyMap() 匹配任何非空Map
anyString() 匹配任何非空String
contains(String substring) 参数包含给定的substring字符串
argThat(ArgumentMatcher matcher) 创建自定义的参数匹配

自定义参数匹配

@Test
public void test2(){
   Person person=mock(Person.class);
   //自定义输入字符长度为偶数时,输出面条。
   Mockito.when(person.eat(Mockito.argThat(new ArgumentMatcher<String>() {
       @Override
       public boolean matches(String argument) {
           return argument.length()% ==;
       }
   }))).thenReturn("面条");

    //输出面条
    System.out.println(person.eat("12"));
}
           

其他方法

方法名 方法描述
reset(T … mocks) 重置Mock
inOrder(Object… mocks) 验证执行顺序
spy(Class classToSpy) 实现调用真实对象的实现
@InjectMocks注解 自动将模拟对象注入到被测试对象中

inOrder 验证执行顺序

Android 单元测试之Mockito框架的使用前序Mock的概念集成Mocktio四种Mock方式验证互动(Interactions)设置桩(Stub)验证模式(Verification Mode)参数匹配器(Argument Matcher)其他方法

Spy

要知道如果Mock一个对象后,这个Mock对象对于所有非Void返回方法将返回默认值(对象则返回null),所有Void方法将什么都不做

如果要保留原来对象的功能,而仅仅修改一个或几个方法的返回值,可以采用Spy方法

Android 单元测试之Mockito框架的使用前序Mock的概念集成Mocktio四种Mock方式验证互动(Interactions)设置桩(Stub)验证模式(Verification Mode)参数匹配器(Argument Matcher)其他方法

上述代码可以看到Spy方法没有改变ArrayList里的方法,只是当get(0)时返回1。

特别注意这个Spy方法看上去似乎很方便,实际上如果你Spy一个需要Mock的对象,就会提示你该对象没有Mock,就比如TextView。

实际上即使你看完前面全部内容,还是不能解决我们

之前使用Log.i(“tag”,”msg”);的时候,单元测试会失败并且提示:

这是因为JUnit并不能在纯Java层面做测试,使用非纯Java API就会报错。这需要一些Mock框架来帮助我们进行测试,这个后面抽空会写一篇新的博文介绍。

要Mock静态方法有两个方法,

  • 一个是使用PowerMock来扩展Mockito
  • 另外一个就是创建一个StaticWrapper来把静态方法变成非静态方法

方法如下:

public class LogTest {

    class StaticWrapper {//包裹静态方法为非静态方法
        void i(String tag, String msg) {
            Log.i(tag, msg);
        }
    }

    @Test
    public void test() {
        StaticWrapper mockedLog = Mockito.mock(StaticWrapper.class);
        mockedLog.i("test", "test");
        Mockito.verify(mockedLog).i("test", "test");
    }

}