本文主要针对测试框架 Mockito
在实践中的经常用到的代码做一示例汇总,并对其实现思想做以简单的分析。
介绍
用来为提供函数返回结果的模拟(mock)及对函数调用过程的验证。
** 关键词 **
- mock : 针对真实的类或者对象,创建一个模拟(代理)的对象。
- stub : 针对一个类或者对象的方法,进行模拟调用及输出。
其中 mock 针对是类和队形,而 stub 针对的是行为。他们具体在此框架中的体现分别是: 1) mock 对应的是类 Mockito
中的 mock
及 spy
方法;2)stub 对应是 Mockito
中的 when
及 doReturn
等系列方法。
PS: 这里注意与框架 Robolectric 的
Shadow
以区别。
引入
testCompile 'org.mockito:mockito-core:2.1.0-beta.119'
代码示例:地址
1. Mock 方法的使用
@Test
public void testMock() {
List mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//verification
verify(mockedList).add("one");
verify(mockedList).clear();
}
可直接通过接口来进行 mock。一旦创建了一个 mock 之后,他会记住所有它的操作,则我们就可以通过 verify 方法来检查相应方法是否调用。
2.打桩(Stub),即调用返回的结果模拟
LinkedList mockedList = mock(LinkedList.class);
//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
//following prints "first"
System.out.println(mockedList.get(0));
//following throws runtime exception
System.out.println(mockedList.get(1));
//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
这里指定关键字 when
返回一个 OngoingStubbing
接口,通过其提供的 thenReturn
,thenThrow
,thenCallRealMethod
及自定义 thenAnswer
来返回相应的结果。
3.参数匹配
LinkedList mockedList = mock(LinkedList.class);
//stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");
//following prints "element"
System.out.println(mockedList.get(999));
//you can also verify using an argument matcher
verify(mockedList).get(anyInt());
有时我们针对函数参数的模拟,不是一个特定的数值,而是一个范围。这时可以范围型的参数匹配,在 ArgumentMatchers
中,提供了一组不同类型的 any 操作。如:any(Class)
,anyObject()
,anyVararg()
,anyChar()
,anyInt()
,anyBoolean()
,anyCollectionOf(Class)
等。
4.调用次数
LinkedList mockedList = mock(LinkedList.class);
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//following two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
//exact number of invocations verification
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
//verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened");
//verification using atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("twice");
verify(mockedList, atMost(5)).add("three times");
通过 times
,never
,atLeastOnce
,atLeast
,atMost
这些方法,我们可以对一个方法的调用次数做判断。其中 times(1)
是默认的。
5.方法添加异常
LinkedList mockedList = mock(LinkedList.class);
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();
使用 doThrow
可以为一个方法的调用添加异常。这样可以验证我们的代码对异常的处理能力如何。
6.顺序验证
// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);
//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");
//create an inOrder verifier for a single mock
InOrder inOrder1 = inOrder(singleMock);
//following will make sure that add is first called with "was added first, then with "was added second"
inOrder1.verify(singleMock).add("was added first");
inOrder1.verify(singleMock).add("was added second");
// B. Multiple mocks that must be used in a particular order
List firstMock = mock(List.class);
List secondMock = mock(List.class);
//using mocks
firstMock.add("was called first");
secondMock.add("was called second");
//create inOrder object passing any mocks that need to be verified in order
inOrder1 = inOrder(firstMock, secondMock);
//following will make sure that firstMock was called before secondMock
inOrder1.verify(firstMock).add("was called first");
inOrder1.verify(secondMock).add("was called second");
若是我们需要对调用的顺序做判断,就可以使用 InOrder
这个类,通过 Mockito 的方法 inOrder
,来作为其参数,这样我们的方法就必须按顺序调用。试试将上述代码的 verify 顺序交换,看看会发生什么。
7.调用从未发生
List mockOne = mock(List.class);
List mockTwo = mock(List.class);
List mockThree = mock(List.class);
//using mocks - only mockOne is interacted
mockOne.add("one");
//ordinary verification
verify(mockOne).add("one");
//verify that method was never called on a mock
verify(mockOne, never()).add("two");
//verify that other mocks were not interacted
verifyZeroInteractions(mockTwo, mockThree);
通过 never
来指定一个方法从未发生调用,使用 verifyZeroInteractions
来确定对象的实例从未发生调用
8. 没有更多调用
List mockedList = mock(List.class);
//using mocks
mockedList.add("one");
mockedList.add("two");
verify(mockedList).add("one");
//following verification will fail
verifyNoMoreInteractions(mockedList);
代码中的 verifyNoMoreInteractions
会发生错误,原因就在于未对 add("two")
做验证,我们在 verify(mockedList).add("one");
代码后添加 add(two)
的方法验证,最后的测试通过。
1.这里的 verify
add("one")
及add("two)
顺序是无所谓的。
2.可以看出的是这个测试方法的不精确性,尽力避免使用。
9. @Mock 注解
public class ArticleManagerTest {
@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;
private ArticleManager manager;
可以通过对属性添加 @Mock 注解来避免使用 mock 方法,不过不要忘了 initMocks 方法的调用:
MockitoAnnotations.initMocks(testClass);
10. 连续调用
HashMap mock = mock(HashMap.class);
when(mock.get("some arg")).thenThrow(new RuntimeException()).thenReturn("foo");
//First call: throws runtime exception:
try {
mock.get("some arg");
} catch (Exception e) {
System.out.println(e.toString());
}
//Second call: prints "foo"
System.out.println(mock.get("some arg"));
//Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.get("some arg"));
通过对 mock 一直添加 then
的返回值,使得我们按顺序每次调用的返回结果都不同。另外,一个简单的写法, thenReturn
支持数组参数,来设定结果依次返回:
when(mock.someMethod("some arg")) .thenReturn("one", "two", "three");
11.Answer 结果返回
HashMap mock = mock(HashMap.class);
when(mock.get(anyString())).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + args[0];
}
});
//the following prints "called with arguments: foo"
System.out.println(mock.get("foo"));
当我们一个函数方法返回结果的不确定性,需要动态地根据参数指来改变。则上述的几个 then
方法不满足的情况下,我们可以通过 thenAnswer
方法返回一个 Answer 对象,来动态地返回结果。
12.doReturn | doThrow | doAnswer | doNothing | doCallRealMethod
List mockedList = mock(LinkedList.class);
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();
使用 do 系列的方法,我们可以针对 返回值
的方法进行测试。
13.检测真实的对象
List list = new LinkedList();
List sypList = spy(list);
//optionally, you can stub out some methods:
when(sypList.size()).thenReturn(100);
//using the spy calls *real* methods
sypList.add("one");
sypList.add("two");
//prints "one" - the first element of a list
System.out.println(sypList.get(0));
//size() method was stubbed - 100 is printed
System.out.println(sypList.size());
//optionally, you can verify
verify(sypList).add("one");
verify(sypList).add("two");
mock 方法是根据接口、类动态地生成一个对象,若是我们有一个真正的对象的时候,其就不适用了,这时,可以使用 spy 方法。但是其有使用限制的:
List list = new LinkedList();
List spy = spy(list);
//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
//when(spy.get(0)).thenReturn("foo");
//You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);
使用 when
+ thenReturn
,并不返回我们预期的结果,而是需要使用 doReturn
+ when
的格式。
其原因在于,Mockito 框架并不会对真实的对象进行 mock,只会真实的对象创建一个副本。
14.指定返回信息
Map mock = mock(HashMap.class, Mockito.RETURNS_SMART_NULLS);
System.out.println(mock.get("b"));
添加了 Mockito. RETURNS_SMART_NULLS
参数,当调用未指定返回行为的方法,输出的内容将不再是简单的 null
异常,而是下面更加人性化的信息:
SmartNull returned by this unstubbed method call on a mock:
hashMap.get("b");
15.参数匹配判断
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]";
}
}
List mock = mock(List.class);
when(mock.addAll(argThat(new ListOfTwoElements()))).thenReturn(true);
mock.addAll(Arrays.asList("one", "two"));
verify(mock).addAll(argThat(new ListOfTwoElements()));
实现 ArgumentMatcher
类,并通过 argThat
方法对参数进行判断。
16.对真实类的部分 mock
这里一般有两种写法:
1) 使用 spy
@Test
public void testPartialRealMock1() {
//you can create partial mock with spy() method:
LinkedList linkedList = new LinkedList();
linkedList.addFirst(1);
List list = spy(linkedList);
assertThat(list.get(0), is(1));
}
通过 spy 调用对象的方法,将会调用其真正的方法。
- 使用 mock
@Rule
public ExpectedException thrown= ExpectedException.none();
@Test
public void testPartialRealMock2() {
//you can enable partial mock capabilities selectively on mocks:
List mock = mock(LinkedList.class);
//Be sure the real implementation is 'safe'.
//If real implementation throws exceptions or depends on specific state of the object then you're in trouble.
when(mock.get(anyInt())).thenCallRealMethod();
thrown.expect(Exception.class);
mock.get(0);
}
针对 mock 的使用时,主要代码在于方法 thenCallRealMethod()
,但它有个很大的安全隐患,就是此方法抛出异常的问题。上述代码就可以看出,因为真实的 list 对象,并不含有任何元素,所以在通过真实方法返回时,就会有异常产生。
这里,建议使用方法一 spy
,来对真实的对象进行测试。
17.重置 mock
List mock = mock(List.class);
when(mock.size()).thenReturn(10);
mock.add(1);
reset(mock);
assertThat(mock.size(), is(0));
使用 reset
方法,可以将 mock 重置为初始状态。
18.序列化 mock
List serializableMock = mock(List.class, withSettings().serializable());
若是 spy 的使用则如下:
List<Object> list = new ArrayList<Object>();
List<Object> spy = mock(ArrayList.class, withSettings() .spiedInstance(list) .defaultAnswer(CALLS_REAL_METHODS) .serializable());
19.timeout 的使用
List mock = mock(List.class);
when(mock.get(0)).thenReturn(1);
System.out.println(mock.get(0));
verify(mock, timeout(100)).get(0);
//above is an alias to:
verify(mock, timeout(100).times(1)).get(0);
System.out.println(mock.get(0));
verify(mock, timeout(100).times(2)).get(0);
verify(mock, timeout(100).atLeast(2)).get(0);
verify(mock, new Timeout(100, new VerificationMode() {
@Override
public void verify(VerificationData data) {
}
@Override
public VerificationMode description(String description) {
return null;
}
})).get(0);
指定了 timeout
的延时,同时我们也可以其他的验证操作,例如 times
,atLeast
等,另外,我们也可以自定义自己的验证规则 VerficationMode
。
20.ignoreStub方法
//mocking lists for the sake of the example (if you mock List in real you will burn in hell)
List mock1 = mock(List.class), mock2 = mock(List.class);
//stubbing mocks:
when(mock1.get(0)).thenReturn(10);
when(mock2.get(0)).thenReturn(20);
//using mocks by calling stubbed get(0) methods:
//System.out.println(mock1.get(0)); //prints 10
System.out.println(mock2.get(0)); //prints 20
mock1.get(0);
verify(mock1).get(0);
//using mocks by calling clear() methods:
mock1.clear();
mock2.clear();
//verification:
verify(mock1).clear();
verify(mock2).clear();
//verifyNoMoreInteractions() fails because get() methods were not accounted for.
try {
verifyNoMoreInteractions(mock1, mock2);
} catch (NoInteractionsWanted e) {
System.out.println(e);
}
//However, if we ignore stubbed methods then we can verifyNoMoreInteractions()
verifyNoMoreInteractions(ignoreStubs(mock1, mock2));
当第一次调用 verifyNoMoreInteractions
时,直接出现异常,是因为之前也调用了 mock2.get(0)
,但是并没有进行 verify
。
而一旦我们对添加了 ignoreStubs
方法,则会忽略之前的 Stub
的方法,不会再有 verify
的限制。
比较特殊的是 inOrder
的方法,它会自带 ignoreStubs
的效果:
List list = mock(List.class);
when(list.get(0)).thenReturn("foo");
list.add(0);
System.out.println(list.get(0)); //we don't want to verify this
list.clear();
//verify(list).add(0);
//verify(list).add(0);
//verify(list).clear();
// Same as: InOrder inOrder = inOrder(list);
InOrder inOrder = inOrder(ignoreStubs(list));
inOrder.verify(list).add(0);
// this will have an error..
//inOrder.verify(list).get(0);
inOrder.verify(list).clear();
inOrder.verifyNoMoreInteractions();
代码中特殊的一点是使用了 inOrder
,它并不会上面 System.out.println(list.get(0));
做处理。
21. 获取 mock 详情
List list = mock(List.class);
assertThat(Mockito.mockingDetails(list).isMock(), is(true));
assertThat(Mockito.mockingDetails(list).isSpy(), is(false));
22.自定义错误信息
List list = mock(List.class);
when(list.get(0)).thenReturn(1);
verify(list, description("should print the get(0) result")).get(0)
官方文档还提供一些关于 Java8 函数式的更多用法,这里因为环境问题就不列举了,更多内容可查阅官方文档。
原理简单剖析
通过上面的示例,我们可以发现两个很重要的方法:mock
及 verify
。
1.mock 类生成
这里是使用运行时生成代码的库 byte-buddy,而对应在 mockito
框架中实现的代码是在 MockBytecodeGenerator
类中。其中主要的代码在方法 generateMockClass
中,
public <T> Class<? extends T> generateMockClass(MockFeatures<T> features) {
DynamicType.Builder<T> builder =
byteBuddy.subclass(features.mockedType)
.name(nameFor(features.mockedType))
.ignoreAlso(isGroovyMethod())
.annotateType(features.mockedType.getAnnotations())
.implement(new ArrayList<Type>(features.interfaces))
.method(any())
.intercept(MethodDelegation.to(DispatcherDefaultingToRealMethod.class))
.transform(Transformer.ForMethod.withModifiers(SynchronizationState.PLAIN))
.attribute(MethodAttributeAppender.ForInstrumentedMethod.INCLUDING_RECEIVER)
.serialVersionUid(42L)
.defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE)
.implement(MockAccess.class)
.intercept(FieldAccessor.ofBeanProperty())
.method(isHashCode())
.intercept(to(MockMethodInterceptor.ForHashCode.class))
.method(isEquals())
.intercept(to(MockMethodInterceptor.ForEquals.class));
if (features.crossClassLoaderSerializable) {
builder = builder.implement(CrossClassLoaderSerializableMock.class)
.intercept(to(MockMethodInterceptor.ForWriteReplace.class));
}
return builder.make()
.load(new MultipleParentClassLoader.Builder()
.append(features.mockedType)
.append(features.interfaces)
.append(Thread.currentThread().getContextClassLoader())
.append(MockAccess.class, DispatcherDefaultingToRealMethod.class)
.append(MockMethodInterceptor.class,
MockMethodInterceptor.ForHashCode.class,
MockMethodInterceptor.ForEquals.class).build(),
ClassLoadingStrategy.Default.INJECTION.with(features.mockedType.getProtectionDomain()))
.getLoaded();
}
这里便是通过 byte-buddy 来生成我们的 mock 类, 其中代码行 .intercept(MethodDelegation.to(DispatcherDefaultingToRealMethod.class))
则是用来生成代理方法的类 ,其中 DispatcherDefaultingToRealMethod
是类 MockMethodInterceptor
的静态内部类。在对其调用时,最后会调到 MockHandlerImpl
类的实现方法 handle
,这个才是我们执行 mock 类方法每次调用的重头戏:
public Object handle(Invocation invocation) throws Throwable {
if (invocationContainerImpl.hasAnswersForStubbing()) {
// 对 doThrow() 或者 doAnswer() 返回 void 格式的执行调用
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
mockingProgress().getArgumentMatcherStorage(),
invocation
);
invocationContainerImpl.setMethodForStubbing(invocationMatcher);
return null;
}
// 验证规则获取
VerificationMode verificationMode = mockingProgress().pullVerificationMode();
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
mockingProgress().getArgumentMatcherStorage(),
invocation
);
// mock 进度状态的验证
mockingProgress().validateState();
// 当 verificationMode 不是空的时候,则表明在执行 verify 方法
if (verificationMode != null) {
// 检查 verificationMode 是否对应正确的 mock
if (((MockAwareVerificationMode) verificationMode).getMock() == invocation.getMock()) {
VerificationDataImpl data = createVerificationData(invocationContainerImpl, invocationMatcher);
verificationMode.verify(data);
return null;
} else {
// 对应的不是相同的 mock , 重新添加 verification mode
mockingProgress().verificationStarted(verificationMode);
}
}
// 对调用执行打桩
invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);
OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainerImpl);
mockingProgress().reportOngoingStubbing(ongoingStubbing);
// 对这次调用,查找是否有存在的 answer
StubbedInvocationMatcher stubbedInvocation = invocationContainerImpl.findAnswerFor(invocation);
if (stubbedInvocation != null) {
stubbedInvocation.captureArgumentsFrom(invocation);
return stubbedInvocation.answer(invocation);
} else {
Object ret = mockSettings.getDefaultAnswer().answer(invocation);
new AnswersValidator().validateDefaultAnswerReturnedValue(invocation, ret);
// 重新设置调用的方法
invocationContainerImpl.resetInvocationForPotentialStubbing(invocationMatcher);
return ret;
}
}
代码中可以看出这个代理方法也做验证及调用方法的记录,用来方便后续 verify 方法的验证。
另外,针对真实对象模拟的方法 spy
,其调用的也是 mock
方法,不同的是指定了 spiedInstance
或者 answer 指定的是 CALLS_REAL_METHODS
。
2. verify 方法的实现
可知 verify 是对 mock 对象的验证,其调用的方法:
public <T> T verify(T mock, VerificationMode mode) {
if (mock == null) {
throw nullPassedToVerify();
}
if (!isMock(mock)) {
throw notAMockPassedToVerify(mock.getClass());
}
MockingProgress mockingProgress = mockingProgress();
VerificationMode actualMode = mockingProgress.maybeVerifyLazily(mode);
mockingProgress.verificationStarted(new MockAwareVerificationMode(mock, actualMode));
return mock;
}
通过获取到 mockingProgress,调用其方法 verificationStarted
,将新的规则 actualMode
保存下来,并最后返回 mock 对象。之后,若是针对 verify 的对象调用方法,则会调到上文提到 MockHandlerImpl
的 handle
方法,会执行下面的语句:
if (((MockAwareVerificationMode) verificationMode).getMock() == invocation.getMock()) {
VerificationDataImpl data = createVerificationData(invocationContainerImpl, invocationMatcher);
verificationMode.verify(data);
return null;
}
这里的 VerificationDataImpl
则有两个属性:
private final InvocationMatcher wanted;
private final InvocationContainer invocations;
其中 invocations
保存着我们对 mock 对象的调用记录,而 wanted
则是我们需要 verify
的方法。而具体的验证规则也就是我们之前保存的 VerificationMode
。
总结
至此,我们总结了 Mockito
框架的多种使用方法,及其简单的原理实现。若是有小伙伴不甚明了,欢迎加入 qq 群:289926871 来一起讨论。