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