什么是 Mock
mock 的中文譯為: 仿制的,模擬的,虛假的。對于測試框架來說,即構造出一個模擬/虛假的對象,使我們的測試能順利進行下去。
為什么要使用 Mock 測試框架
單元測試是為了驗證我們的代碼運行正確性,我們注重的是代碼的流程以及結果的正確與否。而對比于真實運行代碼,可能其中有一些外部依賴的構建步驟相對麻煩,如果我們還是按照真實代碼的構建規則構造出外部依賴,會大大增加單元測試的工作,代碼也會參雜太多非測試部分的內容,測試用例顯得復雜難懂,而采用 Mock 框架,我們可以虛擬出一個外部依賴,只注重代碼的流程與結果,真正地實現測試目的。
Mock 測試框架好處
- 可以很簡單的虛擬出一個復雜對象(比如虛擬出一個接口的實現類)
- 可以配置 mock 對象的行為
- 可以使測試用例只注重測試流程與結果
- 減少外部類或系統帶來的副作用
······
Mockito 簡介
Most popular Mocking framework for unit tests written in Java
Mockito 是當前最流行的單元測試 mock 框架。
使用
在 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/
mockiot-android
用于儀器測試,即需要運行android設備進行測試,其測試代碼路徑位于: module-name/src/androidTest/java/
更多詳細信息,請查看官網:測試應用
ps:
mockito-core
最新版本可以在 Maven 中查詢:mockito-core
mockito-android
最新版本可以在 Maven 中查詢:mockito-android
示例
- 普通單元測試使用 mockito(
mockito-core
),路徑:module-name/src/test/java/
這里摘用官網的Demo:
now you can verify interactions -- 檢驗調對象相關行為是否被調用
import static org.mockito.Mockito.*;
// mock creation
List mockedList = mock(List.class);
// using 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
會通過。
stub method calls -- 配置/方法行為
// 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 對象的行為結果(mockLinkedList
對象為mockedList
,指定其行為get(0)
返回結果為"first"
)。 -
mockedList.get(999)
由于mockedList
沒有指定get(999)
的行為,所以其結果為null
。因為 Mockito 的底層原理是使用 cglib 動態生成一個代理類對象,因此,mock 出來的對象其實質就是一個代理,該代理在沒有配置/指定行為的情況下,默認返回空值:
- Android單元測試使用 mockito(
mockito-android
),路徑:module-name/src/androidTest/java/,該測試需運行安卓真機/模擬器。
上面的 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)
去實例化出注解的成員變量,我們就無需手動進行初始化了。
ps:上面要注意的一個點就是,斷言使用的是開源庫:Truth
更多Demo,請查看:Mockito
Mockito 一些重要方法簡介
-
@Mock/mock
:實例化虛擬對象
//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/Bollean
則返回false
。行為配置(
stub
)是可以被復寫的:比如通常的對象行為是具有一定的配置,但是測試方法可以復寫這個行為。請謹記行為復寫可能表明潛在的行為太多了。一旦配置了行為,方法總是會返回配置值,無論該方法被調用了多少次。
最后一次行為配置是更加重要的 - 當你為一個帶有相同參數的相同方法配置了很多次,最后一次起作用。
-
Argument matchers - 參數匹配
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()
等方法。
- Verifying exact number of invocations / at least x / never - 校驗次數
//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("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次 |
4. 配置返回類型為void
的方法拋出異常:doThrow
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();
5. Verification in order - 按順序校驗
有時對于一些行為,有先后順序之分,所以,當我們在校驗時,就需要考慮這個行為的先后順序:
// 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 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);
//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
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");
// Oh, and A + B can be mixed together at will
-
Stubbing consecutive calls (iterator-style stubbing) - 存根連續調用
對于同一個方法,如果我們想讓其在多次調用中分別返回不同的數值,那么就可以使用存根連續調用:
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")
-
doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() family of methods
對于返回類型為void
的方法,存根要求使用另一種形式的when(Object)
函數,因為編譯器要求括號內不能存在void
方法。
例如,存根一個返回類型為void
的方法,要求調用時拋出一個異常:
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();
-
Spying on real objects - 監視真實對象
我們前面使用的都是 mock 出來一個對象,這樣,當我們沒有配置/存根其具體行為的話,結果就會返回空類型。而使用特務對象(spy
),那么對于我們沒有存根的行為,它會調用原來對象的方法。可以把spy
想象成 局部mock。
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
//using 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();
-
Aliases for behavior driven development (Since 1.8.0) - 測試驅動開發
以行為驅動開發格式使用 //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());
}
- Custom verification failure message (Since 2.1.0) - 自定義錯誤校驗輸出信息
// 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();
11.@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
-- 參數捕捉
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()
更多限制點,請查看:FAQ
PowerMockito
針對 Mocktio 無法mock
靜態方法等限制,使用 PowerMockito 則可以解決這一限制。
- PowerMockito使用方法
- Dowanload:
testImplementation "org.mockito:mockito-core:2.8.47"
testImplementation 'org.powermock:powermock-api-mockito2:1.7.1'
testImplementation 'org.powermock:powermock-module-junit4:1.7.1'
詳情請查看:Mockito 2 Maven
注: 上面只所以不使用最新的 Mockito 版本,是因為根據 PowerMockito 官方文檔,目前 PowerMockito 版本對應支持的 Mockito 版本如下圖所示:
因此,這里就選擇 Mockito 2.8.47(2.8.x最新版本)
- 示例
//static method
public class Static {
public static String firstStaticMethod() {
return "I am a firstStatic method";
}
public static String secondStaticMethod() {
return "I am a secondStatic method";
}
}
//