前序
首先我们要理解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");
}
}