單元測試與Mockito

2023-04-22 21:01:39

系列文章目錄和關於我

零丶背景

最近在新公司第一次上手寫程式碼,寫了一個不是很難的業務邏輯程式碼,但是在我寫單元測試的時候,發現自己對單元測試的理解的就是一坨,整個過程寫得慢,還寫得臭。造成這種局面我認為是因為:

  • 對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控制其行為。

3.1mock方法->呼叫方法->驗證方法

3.1 模擬OtherClient丟擲異常

3.2 模擬OtherClient返回空Map

3.3模擬OtherClient返回非空Map