简介
笔者的Android单元测试相关系列:
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模式下如何进行单元测试的编写。