本文主要針對測試框架 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 來一起討論。