零丶背景
最近在新公司第一次上手寫程式碼,寫了一個不是很難的業務邏輯程式碼,但是在我寫單元測試的時候,發現自己對單元測試的理解的就是一坨,整個過程寫得慢,還寫得臭。造成這種局面我認為是因為:
- 對Mockito api是不是很熟悉
- 沒有自己單元測試方法論,不知道怎樣寫好單元測試。
now,我將從這兩個部分來學習一下單元測試,如何寫,如何寫好單元測試?
一丶為什麼需要單元測試
在上一份工作,我基本上不咋寫單元測試,覺得很麻煩,不如直接postman,swagger開衝,這種顯然不容易覆蓋到所有的case。
單元測試的好處:
-
增強信心
單元測試覆蓋率越高,我們越對自己的程式碼有信心。
-
揭示意圖
寫單元測試的時候,我們是明確自己的程式碼到底是出於什麼目的寫的
-
安全重構
不只是重構,哪怕後續在原有功能上進行新增,透過執行之前存在單元測試有助於我們驗證,我們沒有影響到原有功能。
-
快速反饋
寫單元測試的過程,我們其實有可能發現自己程式碼存在的缺陷,透過單元測試直白的報錯,我們可以很快得到反饋,這個反饋速度是測試滴滴你所不具備的。
-
定位缺陷
單元測試並不能幫我們找出所有存在的bug(測試同事:沒事,我會出手),但是我們發現bug後,可以將輸入放在單元測試中進行回放,直到可以重現並定位到問題,然後使用這種情況的case來補充單元測試用例。
二丶引入依賴&這些依賴的作用
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.7.7</version>
<scope>test</scope>
</dependency>
-
junit
提供了許多方便使用的註解,標註在方法上
-
Mockito
Mockito 是一種 Java Mock 框架,主要就是用來做 Mock 測試的,可以模擬出一個物件、模擬方法的返回值、模擬丟擲異常,模擬靜態方法等等,同時也會記錄呼叫這些模擬方法的引數、呼叫順序,從而可以校驗出這個 Mock 物件是否有被正確的順序呼叫,以及按照期望的引數被呼叫。
Mock 測試:比如我們的Service依賴其他的服務提供的介面方法,使用mock可以模擬出這個介面的表現(正常返回,丟擲異常等到)從而讓單元測試不那麼依賴外部的服務。
-
powermock
可以看作是mock增強版本,提供模擬私有方法等功能,我們這裡沒有進行引入。
三丶Mockito 常用功能
0.從一個例子開始
如上圖,我們的MyService依賴於OtherClient,這個OtherClient可能由於網路原因會出現錯誤,或者其他情況丟擲異常,我們的MyService需要進行處理。
1.@InjectMocks & @Mock &MockitoAnnotations.openMocks
- @InjectMocks:標記應進行注射的欄位,類似於spring的依賴注入,但是這裡會使用Mock產生的物件
- @Mock :將欄位標記為模擬欄位,我們可以使用Mockito提供的方法來 打樁。
- MockitoAnnotations.openMocks:開啟Mockito註解的功能
2.打樁
打樁可以理解為 mock 物件規定它的行為,使其按照我們的要求來執行具體的操作。
2.1 指定入參讓mock物件返回指定物件——thenReturn
//讓client在query入參為1的時候,返回100為key,aaa為value的單鍵值對的map
Mockito.when(client.query(1)).thenReturn(new HashMap<>(Collections.singletonMap(100, "aaaa")));
Map<Integer, String> res = client.query(1);
Assert.assertEquals(1, res.size());
Assert.assertEquals(res.get(100), "aaaa");
2.2 指定入參讓mock物件丟擲異常——thenThrow
Mockito.when(client.query(2)).thenThrow(new RuntimeException("222"));
Assert.assertThrows("222", RuntimeException.class, () -> client.query(2));
2.3 指定任何引數都執行指定操作——Mockito.anyInt()
Mockito.when(client.query(Mockito.anyInt())).thenReturn(new HashMap<>());
Assert.assertEquals(0, client.query(-1).size());
2.4 引數匹配器——ArgumentMatcher
有時候,我們希望入參入參符合要的時候,mock物件進行什麼操作。
如下,我們要求mock物件在輸入引數是1, 2, 3
的時候返回空map
HashSet<Integer> integers = new HashSet<>(Arrays.asList(1, 2, 3));
Mockito.when(client.query(Mockito.argThat(new ArgumentMatcher<Integer>() {
@Override
public boolean matches(Integer argument) {
return integers.contains(argument);
}
}))).thenReturn(Collections.emptyMap());
Assert.assertEquals(0,client.query(2).size());
2.5 控制mock物件返回結果——thenAnswer
有時候我們希望mock物件可以根據輸出的不同返回不同的結果,符合我們要求的結果。
如下,我們使用thenAnswer根據入參返回不同的結果。
Mockito.when(client.query(Mockito.anyInt())).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Integer argument = invocation.getArgument(0);
String str = argument%2==0?"偶數":"奇數";
return new HashMap<Integer,String>(Collections.singletonMap(argument,str));
}
});
Assert.assertEquals("偶數", client.query(2).get(2));
2.6 讓mock物件呼叫真實方法——thenCallRealMethod
上面都是說mock物件如何去控制輸出,thenCallRealMethod可以讓mock物件執行真實的邏輯。
Mockito.when(client.query(-1)).thenCallRealMethod();
2.7 驗證——verify
verify可以讓我們驗證當前mock物件,比如下面驗證client至少執行了四次query
//驗證 client.query最起碼呼叫了4次
Mockito.verify(client,Mockito.atLeast(4)).query(Mockito.anyInt());
2.8 mock靜態方法——mockStatic
有時候靜態方法也需要進行mock控制,可以使用
四丶一個有依賴的單元測試
0.還是這個例子
如上圖,我們的MyService依賴於OtherClient,這個OtherClient可能由於網路原因會出現錯誤,或者其他情況丟擲異常,我們的MyService需要進行處理。
1.確認需要mock什麼
上面這個例子中,OtherClient是外部提供給我們的介面,它存在一定的機率失敗,在單元測試的過程我們需要mock它的行為,而不是真的去呼叫外部介面。
2.定義物件,前置準備
這裡我們得明確 MyService是我們需要測試的,那就別mock它,OtherClient是外部依賴,需要進行mock控制其行為。