前言
Mockito
是當前最流行的 單元測試 Mock
框架。採用 Mock
框架,我們可以 虛擬 出一個 外部依賴,降低測試 元件 之間的 耦合度,只注重程式碼的 流程與結果,真正地實現測試目的。
正文
什麼是Mock
Mock
的中文譯為仿製的,模擬的,虛假的。對於測試框架來說,即構造出一個模擬/虛假的物件,使我們的測試能順利進行下去。
Mock
測試就是在測試過程中,對於某些 不容易構造(如 HttpServletRequest
必須在 Servlet
容器中才能構造出來)或者不容易獲取 比較複雜 的物件(如 JDBC
中的 ResultSet
物件),用一個 虛擬 的物件(Mock
物件)來建立,以便測試方法。
為什麼使用Mock測試
單元測試 是為了驗證我們的程式碼執行正確性,我們注重的是程式碼的流程以及結果的正確與否。
對比真實執行程式碼,可能其中有一些 外部依賴 的構建步驟相對麻煩,如果我們還是按照真實程式碼的構建規則構造出外部依賴,會大大增加單元測試的工作,程式碼也會參雜太多非測試部分的內容,測試用例顯得複雜難懂。
採用 Mock
框架,我們可以 虛擬 出一個 外部依賴,只注重程式碼的 流程與結果,真正地實現測試目的。
Mock測試框架的好處
- 可以很簡單的虛擬出一個複雜物件(比如虛擬出一個介面的實現類);
- 可以配置
mock
物件的行為; - 可以使測試用例只注重測試流程與結果;
- 減少外部類、系統和依賴給單元測試帶來的耦合。
Mockito的流程
如圖所示,使用 Mockito
的大致流程如下:
-
建立 外部依賴 的
Mock
物件, 然後將此Mock
物件注入到 測試類 中; -
執行 測試程式碼;
-
校驗 測試程式碼 是否執行正確。
Mockito的使用
在 Module
的 build.gradle
中新增如下內容:
dependencies {
//Mockito for unit tests
testImplementation "org.mockito:mockito-core:2.+"
//Mockito for Android tests
androidTestImplementation `org.mockito:mockito-android:2.+`
}
複製程式碼
這裡稍微解釋下:
mockito-core
: 用於 本地單元測試,其測試程式碼路徑位於module-name/src/test/java/
mockito-android
: 用於 裝置測試,即需要執行android
裝置進行測試,其測試程式碼路徑位於module-name/src/androidTest/java/
mockito-core最新版本可以在 Maven 中查詢:mockito-core。
mockito-android最新版本可以在 Maven 中查詢:mockito-android
Mockito的使用示例
普通單元測試使用 mockito(mockito-core)
,路徑:module-name/src/test/java/
這裡摘用官網的 Demo
:
檢驗調物件相關行為是否被呼叫
import static org.mockito.Mockito.*;
// Mock creation
List mockedList = mock(List.class);
// Use mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one"); //呼叫了add("one")行為
mockedList.clear(); //呼叫了clear()行為
// Selective, explicit, highly readable verification
verify(mockedList).add("one"); // 檢驗add("one")是否已被呼叫
verify(mockedList).clear(); // 檢驗clear()是否已被呼叫
複製程式碼
這裡 mock
了一個 List
(這裡只是為了用作 Demo
示例,通常對於 List
這種簡單的類物件建立而言,直接 new
一個真實的物件即可,無需進行 mock
),verify()
會檢驗物件是否在前面已經執行了相關行為,這裡 mockedList
在 verify
之前已經執行了 add("one")
和 clear()
行為,所以verify()
會通過。
配置/方法行為
// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);
// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");
// the following prints "first"
System.out.println(mockedList.get(0));
// the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
複製程式碼
這裡對幾個比較重要的點進行解析:
when(mockedList.get(0)).thenReturn(“first”)
這句話 Mockito
會解析為:當物件 mockedList
呼叫 get()
方法,並且引數為 0
時,返回結果為"first"
,這相當於定製了我們 mock
物件的行為結果(mock LinkedList
物件為 mockedList
,指定其行為 get(0)
,則返回結果為 "first"
)。
mockedList.get(999)
由於 mockedList
沒有指定 get(999)
的行為,所以其結果為 null
。因為 Mockito
的底層原理是使用 cglib
動態生成一個 代理類物件,因此,mock
出來的物件其實質就是一個 代理,該代理在 沒有配置/指定行為 的情況下,預設返回 空值。
上面的 Demo
使用的是 靜態方法 mock()
模擬出一個例項,我們還可以通過註解 @Mock
也模擬出一個例項:
@Mock
private Intent mIntent;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void mockAndroid(){
Intent intent = mockIntent();
assertThat(intent.getAction()).isEqualTo("com.yn.test.mockito");
assertThat(intent.getStringExtra("Name")).isEqualTo("Whyn");
}
private Intent mockIntent(){
when(mIntent.getAction()).thenReturn("com.yn.test.mockito");
when(mIntent.getStringExtra("Name")).thenReturn("Whyn");
return mIntent;
}
複製程式碼
對於標記有 @Mock
, @Spy
, @InjectMocks
等註解的成員變數的 初始化 到目前為止有 2
種方法:
-
對
JUnit
測試類新增@RunWith(MockitoJUnitRunner.class)
-
在標示有
@Before
方法內呼叫初始化方法:MockitoAnnotations.initMocks(Object)
上面的測試用例,對於 @Mock
等註解的成員變數的初始化又多了一種方式 MockitoRule
。規則 MockitoRule
會自動幫我們呼叫 MockitoAnnotations.initMocks(this)
去 例項化 出 註解 的成員變數,我們就無需手動進行初始化了。
Mockito的重要方法
例項化虛擬物件
// You can mock concrete classes, not just interfaces
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));
// Although it is possible to verify a stubbed invocation, usually it`s just redundant
// If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
// If your code doesn`t care what get(0) returns, then it should not be stubbed. Not convinced? See here.
verify(mockedList).get(0);
複製程式碼
-
對於所有方法,
mock
物件預設返回null
,原始型別/原始型別包裝類 預設值,或者 空集合。比如對於int/Integer
型別,則返回0
,對於boolean/Boolean
則返回false
。 -
行為配置(
stub
)是可以被複寫的:比如通常的物件行為是具有一定的配置,但是測試方法可以複寫這個行為。請謹記行為複寫可能表明潛在的行為太多了。 -
一旦配置了行為,方法總是會返回 配置值,無論該方法被呼叫了多少次。
-
最後一次行為配置是更加重要的,當你為一個帶有相同引數的相同方法配置了很多次,最後一次起作用。
引數匹配
Mockito
通過引數物件的 equals()
方法來驗證引數是否一致,當需要更多的靈活性時,可以使用引數匹配器:
// Stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");
// Stubbing using custom matcher (let`s say isValid() returns your own matcher implementation):
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
// Following prints "element"
System.out.println(mockedList.get(999));
// You can also verify using an argument matcher
verify(mockedList).get(anyInt());
// Argument matchers can also be written as Java 8 Lambdas
verify(mockedList).add(argThat(someString -> someString.length() > 5));
複製程式碼
引數匹配器 允許更加靈活的 驗證 和 行為配置。更多 內建匹配器 和 自定義引數匹配器 例子請參考:ArgumentMatchers
,MockitoHamcrest
注意:如果使用了引數匹配器,那麼所有的引數都需要提供一個引數匹配器。
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// Above is correct - eq() is also an argument matcher
verify(mock).someMethod(anyInt(), anyString(), "third argument");
// Above is incorrect - exception will be thrown because third argument is given without an argument matcher.
複製程式碼
類似 anyObject()
,eq()
這類匹配器並不返回匹配數值。他們內部記錄一個 匹配器堆疊 並返回一個空值(通常為 null
)。這個實現是為了匹配 java
編譯器的 靜態型別安全,這樣做的後果就是你不能在 檢驗/配置方法 外使用 anyObject()
,eq()
等方法。
校驗次數
LinkedList mockedList = mock(LinkedList.class);
// Use mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// Follow 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("three times");
verify(mockedList, atMost(5)).add("three times");
複製程式碼
校驗次數方法常用的有如下幾個:
Method | Meaning |
---|---|
times(n) | 次數為n,預設為1(times(1)) |
never() | 次數為0,相當於times(0) |
atLeast(n) | 最少n次 |
atLeastOnce() | 最少一次 |
atMost(n) | 最多n次 |
丟擲異常
doThrow(new RuntimeException()).when(mockedList).clear();
// following throws RuntimeException
mockedList.clear();
複製程式碼
按順序校驗
有時對於一些行為,有先後順序之分,所以,當我們在校驗時,就需要考慮這個行為的先後順序:
// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);
// Use a single mock
singleMock.add("was added first");
singleMock.add("was added second");
// Create an inOrder verifier for a single mock
InOrder inOrder = inOrder(singleMock);
// Following will make sure that add is first called with "was added first, then with "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.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);
// Use mocks
firstMock.add("was called first");
secondMock.add("was called second");
// Create inOrder object passing any mocks that need to be verified in order
InOrder inOrder = inOrder(firstMock, secondMock);
// Following will make sure that firstMock was called before secondMock
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
複製程式碼
存根連續呼叫
對於同一個方法,如果我們想讓其在 多次呼叫 中分別 返回不同 的數值,那麼就可以使用存根連續呼叫:
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
// First call: throws runtime exception:
mock.someMethod("some arg");
// Second call: prints "foo"
System.out.println(mock.someMethod("some arg"));
// Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.someMethod("some arg"));
複製程式碼
也可以使用下面更簡潔的存根連續呼叫方法:
when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
複製程式碼
注意:存根連續呼叫要求必須使用鏈式呼叫,如果使用的是同個方法的多個存根配置,那麼只有最後一個起作用(覆蓋前面的存根配置)。
// All mock.someMethod("some arg") calls will return "two"
when(mock.someMethod("some arg").thenReturn("one")
when(mock.someMethod("some arg").thenReturn("two")
複製程式碼
無返回值函式
對於 返回型別 為 void
的方法,存根要求使用另一種形式的 when(Object)
函式,因為編譯器要求括號內不能存在 void
方法。
例如,存根一個返回型別為 void
的方法,要求呼叫時丟擲一個異常:
doThrow(new RuntimeException()).when(mockedList).clear();
// Following throws RuntimeException:
mockedList.clear();
複製程式碼
監視真實物件
前面使用的都是 mock
出來一個物件。這樣,當 沒有配置/存根 其具體行為的話,結果就會返回 空型別。而如果使用 特務物件(spy
),那麼對於 沒有存根 的行為,它會呼叫 原來物件 的方法。可以把 spy
想象成區域性 mock
。
List list = new LinkedList();
List spy = spy(list);
// Optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
// Use the spy calls *real* methods
spy.add("one");
spy.add("two");
// Prints "one" - the first element of a list
System.out.println(spy.get(0));
// Size() method was stubbed - 100 is printed
System.out.println(spy.size());
// Optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");
複製程式碼
注意:由於 spy 是區域性 mock,所以有時候使用 when(Object) 時,無法做到存根作用。此時,就可以考慮使用 doReturn() | Answer() | Throw() 這類方法進行存根:
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);
複製程式碼
spy
並不是 真實物件 的 代理。相反的,它對傳遞過來的 真實物件 進行 克隆。所以,對 真實物件 的任何操作,spy
物件並不會感知到。同理,對 spy
物件的任何操作,也不會影響到 真實物件。
當然,如果使用 mock
進行物件的 區域性 mock
,通過 doCallRealMethod() | thenCallRealMethod()
方法也是可以的:
// You can enable partial mock capabilities selectively on mocks:
Foo mock = mock(Foo.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.someMethod()).thenCallRealMethod();
複製程式碼
測試驅動開發
以 行為驅動開發 的格式使用 //given //when //then 註釋為測試用法基石編寫測試用例,這正是 Mockito
官方編寫測試用例方法,強烈建議使用這種方式測試編寫。
import static org.mockito.BDDMockito.*;
Seller seller = mock(Seller.class);
Shop shop = new Shop(seller);
public void shouldBuyBread() throws Exception {
// Given
given(seller.askForBread()).willReturn(new Bread());
// When
Goods goods = shop.buyBread();
// Then
assertThat(goods, containBread());
}
複製程式碼
自定義錯誤校驗輸出資訊
// Will print a custom message on verification failure
verify(mock, description("This will print on failure")).someMethod();
// Will work with any verification mode
verify(mock, times(2).description("someMethod should be called twice")).someMethod();
複製程式碼
@InjectMock
構造器,方法,成員變數依賴注入
使用 @InjectMock
註解時,Mockito
會檢查 類構造器,方法 或 成員變數,依據它們的 型別 進行自動 mock
。
public class InjectMockTest {
@Mock
private User user;
@Mock
private ArticleDatabase database;
@InjectMocks
private ArticleManager manager;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testInjectMock() {
// Calls addListener with an instance of ArticleListener
manager.initialize();
// Validate that addListener was called
verify(database).addListener(any(ArticleListener.class));
}
public static class ArticleManager {
private User user;
private ArticleDatabase database;
public ArticleManager(User user, ArticleDatabase database) {
super();
this.user = user;
this.database = database;
}
public void initialize() {
database.addListener(new ArticleListener());
}
}
public static class User {
}
public static class ArticleListener {
}
public static class ArticleDatabase {
public void addListener(ArticleListener listener) {
}
}
}
複製程式碼
成員變數 manager
型別為 ArticleManager
,它的上面標識別了 @InjectMocks
。這意味著要 mock
出 manager
,Mockito
需要先自動 mock
出 ArticleManager
所需的 構造引數(即:user
和 database
),最終 mock
得到一個 ArticleManager
,賦值給 manager
。
引數捕捉
ArgumentCaptor
允許在 verify
的時候獲取 方法引數內容,這使得我們能在 測試過程 中能對 呼叫方法引數 進行 捕捉 並 測試。
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Captor
private ArgumentCaptor<List<String>> captor;
@Test
public void testArgumentCaptor(){
List<String> asList = Arrays.asList("someElement_test", "someElement");
final List<String> mockedList = mock(List.class);
mockedList.addAll(asList);
verify(mockedList).addAll(captor.capture()); // When verify,you can capture the arguments of the calling method
final List<String> capturedArgument = captor.getValue();
assertThat(capturedArgument, hasItem("someElement"));
}
複製程式碼
Mocktio的侷限
- 不能
mock
靜態方法; - 不能
mock
構造器; - 不能
mock
equals()
和hashCode()
方法。
歡迎關注技術公眾號:零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。