為什么要mock?
有很多朋友不愿意寫單元測試,覺得寫單測試比較花時間,甚至不會寫單元測試,很大程度上是因為不想寫或者不會寫mock。
mock對于單元測試來很重要。單元測試之所以名字里面有“單元”,就是因為一個測試用例只測很小的一個單元代碼。但我們的代碼總會有依賴,要測試的方法內(nèi)部可能調(diào)用了其它類的方法,這在代碼中是很常見的邏輯。所以我們經(jīng)常會需要mock,用來消除依賴。
所謂mock,翻譯過來就是“模擬”,就是模擬你要測試的代碼里面「依賴的其它對象」,模擬它的輸入和輸出,這樣就不用管其它邏輯是如何實現(xiàn)的,我們「假定它是符合我們期望的就行了」。
可能你會有一個疑問:那我們在單元測試里面假定它符合期望了,但實際運行時它有bug了,并不是符合我們期望的,怎么辦呢?
首先,我們對那個類也會做單元測試,我們用它的單元測試保證了它的邏輯是正確的。
然后,一個完整的測試體系,不應該只有單元測試。單元測試之上,還應該有集成測試、API測試等,從更高的層面來保證整個應用程序能夠如預期工作。
Mockito的基本用法
Mockito是JAVA語言非常主流的一個框架,自己使用起來感覺也比較好用。所以這篇文章想?yún)R總介紹一下Mockito的各種用法,這樣大家在以后寫單元測試和看單元測試的時候,就能夠比較清晰為什么要這么寫。
設(shè)置Mockito環(huán)境
要使用Mockito,首先得在測試類里面設(shè)置好Mockito環(huán)境。這是為了能夠讓單元測試框架(本文主要介紹JUnit)能夠識別和使用Mockito。
在JUnit 4, JUnit使用了@RunWith注解來聲明一個“運行器”。這個運行期的作用是為單元測試提供「mock的初始化工作」(比如使用@Mock、@Spy等注解時,需要初始化),以及「驗證mock語法」的功能。
比如我們可能會經(jīng)常用到的:
@RunWith(JUnit4.class)
@RunWith(SpringRunner.class)
@RunWith(SpringJUnit4ClassRunner.class)
Mockito也有相應的啟動器,在@RunWith注解上面使用這個啟動器就可以使用Mockito的環(huán)境了:
@RunWith(MockitoJUnitRunner)
在JUnit 5,使用了@ExtendWith注解來代替@RunWith注解,Mockito也支持JUnit 5,提供了MockitoExtension類。
除了使用注解以外,也可以使用靜態(tài)方法initMocks來實現(xiàn)這個功能:
MockitoAnnotations.initMocks(this)
mock
mock,即mock一個對象。也是注解和代碼兩種方式可以實現(xiàn)。
@Mock
private User user;
Order order = Mockito.mock(Order.class);
mock對象后,就可以對它使用given等方法模擬它的輸入和輸出。
given(order.getId()).willReturn(1L);
assertEquals(order.getId(), 1L);
spy
mock出來的對象是完全虛擬的,不會真正地調(diào)用本來的實現(xiàn)。如果不對它使用given等方法,會返回默認值(null, 0, false等)。而spy如果不使用given等方法,會調(diào)用這個對象本來的實現(xiàn),返回實際運行后的值。
?
不是很推薦使用spy,因為它沒有消除依賴
?
spy同樣有注解和靜態(tài)方法的方式:
@Spy
private user user;
Order order = Mockito.spy(Order.class);
captor
captor翻譯過來是“捕獲”的意思,主要用來捕捉程序運行時調(diào)用mock或者spy的對象的方法時,傳入的參數(shù)。它支持泛型,即要捕獲的參數(shù)的類型。
@Captor
ArgumentCaptor<User> userCaptor;
ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);
captor一般是與given或者verify等方法配合使用。
@Mock
List mockedList;
@Captor
ArgumentCaptor argCaptor;
@Test
public void whenUseCaptorAnnotation_thenTheSam() {
mockedList.add("one");
Mockito.verify(mockedList).add(argCaptor.capture());
assertEquals("one", argCaptor.getValue());
}
InjectMocks
使用@InjectMocks注解,可以將mock或spy的對象自動注入要測試的對象。這在Spring等使用自動注入的框架里用得非常廣泛。
@Mock
Map<String, String> wordMap;
@InjectMocks
MyDictionary dic = new MyDictionary();
// 類定義:
class MyDictionary {
Map<String, String> wordMap;
public MyDictionary() {
wordMap = new HashMap<String, String>();
}
public void add(final String word, final String meaning) {
wordMap.put(word, meaning);
}
public String getMeaning(final String word) {
return wordMap.get(word);
}
}
打樁
所謂打樁,其實就是mock的一個過程。我們給定期望的輸入和輸入,mock的對象就能夠如我們期望的那樣工作。
打樁有兩種寫法,一種是傳統(tǒng)的寫法,一種是BDD **Behavior-Driven Development (行為驅(qū)動開發(fā))**的寫法。
傳統(tǒng)寫法
我們先看看傳統(tǒng)的寫法,傳統(tǒng)的寫法主要用Mockito類的方法。基本是when-then-invoke-verify形式。
when(phoneBookRepository.contains(momContactName))
.thenReturn(false);
phoneBookService.register(momContactName, momPhoneNumber);
verify(phoneBookRepository)
.insert(momContactName, momPhoneNumber);
BDD寫法
BDD寫法主要是用BDDMockito類的方法,看起來是given–will-invoke-then的形式。
given(phoneBookRepository.contains(momContactName))
.willReturn(false);
phoneBookService.register(momContactName, momPhoneNumber);
then(phoneBookRepository)
.should()
.insert(momContactName, momPhoneNumber);
動態(tài)mock
有時候我們可能會遇到這個問題:在方法內(nèi)部可能有循環(huán)或者遞歸,會多次調(diào)用其它對象的方法,但輸入的參數(shù)不同,我們在mock的時候,期望它根據(jù)不同的輸入?yún)?shù)返回不同的結(jié)果。這個時候如果一個一個寫given,就需要寫很多次。但其實可以用動態(tài)mock來做。它是基于InvocationOnMock來實現(xiàn)的。
given(phoneBookRepository.contains(momContactName))
.willReturn(true);
given(phoneBookRepository.getPhoneNumberByContactName(momContactName))
.will((InvocationOnMock invocation) ->
invocation.getArgument(0).equals(momContactName)
? momPhoneNumber
: null);
phoneBookService.search(momContactName);
then(phoneBookRepository)
.should()
.getPhoneNumberByContactName(momContactName);
Mockito的不足
Mockito可以說是Java語言最流行的mock框架了。但它不是所有對象都可以mock的,有一些限制。
Mockito在3.4.0以前,是不能mock靜態(tài)方法的。這取決于它的底層實現(xiàn),是使用動態(tài)代理來做的。而動態(tài)代理是代理靜態(tài)方法、final方法和private方法的。
一般來說,我們不應該mock靜態(tài)方法、final方法和private方法的。但有時候可能會有這樣的需求,比如Apache和guava框架,就使用了大量的靜態(tài)方法提供一些工具類。如果需要Mock的話,就得配合PowerMock等框架來實現(xiàn)。但PowerMock框架目前還不支持JUnit 5。
在Mockito 3.4.0,開始支持mock靜態(tài)方法,底層是通過修改字節(jié)碼來實現(xiàn)的。但性能很差,mock一個實例大概要一秒多,大家酌情使用。
關(guān)于作者
我是Yasin,一個有顏有料又有趣的程序員。
微信公眾號:編了個程
個人網(wǎng)站:https://yasinshaw.com