Android 单元测试之Mockito框架的使用

前序

首先我们要理解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。

这里写图片描述

集成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());
    }

这里写图片描述

  • 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
  • 设置桩的值可以设置多次,只会返回最后一次设置的值。(如下图)
    这里写图片描述

因为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())

Mockito.doNothing().doThrow(new NullPointerException()).when(mockedTextView).setText("abc");

自定义应答(Answer)

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

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

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

        //指定回调执行操作
        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)

例子

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

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

参数匹配器(Argument Matcher)

  • 有时候我们不关心输入,而是关系输入的类型,以及调用该方法的次数,比如说setText()方法:
    这里写图片描述

常用参数匹配器

方法名方法描述
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()% 2==0;
       }
   }))).thenReturn("面条");

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

其他方法

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

inOrder 验证执行顺序

这里写图片描述

Spy

要知道如果Mock一个对象后,这个Mock对象对于所有非Void返回方法将返回默认值(对象则返回null),所有Void方法将什么都不做
如果要保留原来对象的功能,而仅仅修改一个或几个方法的返回值,可以采用Spy方法
这里写图片描述
上述代码可以看到Spy方法没有改变ArrayList里的方法,只是当get(0)时返回1。
特别注意这个Spy方法看上去似乎很方便,实际上如果你Spy一个需要Mock的对象,就会提示你该对象没有Mock,就比如TextView。

实际上即使你看完前面全部内容,还是不能解决我们
之前使用Log.i(“tag”,”msg”);的时候,单元测试会失败并且提示:

java.lang.RuntimeException: Method i in android.util.Log not mocked.

这是因为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");
    }

}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值