Android單元測試-Mockito 淺析

本文主要針對測試框架 Mockito 在實踐中的經常用到的代碼做一示例匯總,并對其實現思想做以簡單的分析。

介紹

用來為提供函數返回結果的模擬(mock)及對函數調用過程的驗證。

** 關鍵詞 **

  • mock : 針對真實的類或者對象,創建一個模擬(代理)的對象。
  • stub : 針對一個類或者對象的方法,進行模擬調用及輸出。

其中 mock 針對是類和隊形,而 stub 針對的是行為。他們具體在此框架中的體現分別是: 1) mock 對應的是類 Mockito 中的 mockspy 方法;2)stub 對應是 Mockito 中的 whendoReturn 等系列方法。

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 接口,通過其提供的 thenReturnthenThrowthenCallRealMethod 及自定義 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");

通過 timesneveratLeastOnceatLeastatMost 這些方法,我們可以對一個方法的調用次數做判斷。其中 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 調用對象的方法,將會調用其真正的方法。

  1. 使用 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 的延時,同時我們也可以其他的驗證操作,例如 timesatLeast 等,另外,我們也可以自定義自己的驗證規則 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 函數式的更多用法,這里因為環境問題就不列舉了,更多內容可查閱官方文檔。

原理簡單剖析

通過上面的示例,我們可以發現兩個很重要的方法:mockverify

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 的對象調用方法,則會調到上文提到 MockHandlerImplhandle 方法,會執行下面的語句:

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 來一起討論。

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容