單元測試Junit5+Mockito3+Assertj


單元測試介紹與實踐

為什么單元測試

  • 天然的方法說明文檔
  • 代碼質量的保證
  • 持續重構的定心丸

什么是好的單元測試

  • 單元測試需要自動化執行(CI)
  • 單元測試需要快速執行
    • 避免改代1行代碼,單測跑5分鍾的情況,誰也不願意等
  • 單元測試不應該依賴測試的執行順序,UT相互之間是獨立的
  • 單元測試不應該依賴數據庫,文件IO或任何長耗時任務。相反,單元測試需要與外部依賴隔離。
  • 單元測試是持續穩定的,在任何時候,任何環境中都是可執行的
  • 單元測試要有意義
    • get,set沒必要寫ut,核心的業務邏輯要寫
  • 單元測試要盡量簡短,及時維護
    • 太長太復雜的代碼誰也不願意看,看看是不是代碼結構有問題,拆分邏輯單一職責原則(Single Responsibility Principle,SRP)

單元測試原則

AIR原則:

  • Automatic自動化
    • UT應該全部自動執行
    • 不能是通過手動打log驗證UT結果,應該使用斷言來驗證
  • Independent獨立性
    • UT用例之間不勻速相互調用,也不允許有執行次序的依賴
  • Repeatable可重復性
    • 不受外界環境的影響,尤其是一些網絡,服務,中間件等的依賴,要和環境解耦

測試什么

  • 某個類或者函數,粒度要足夠小
  • 常用的輸入輸出組合
    • 正確的輸入,得到正確的輸出
    • 錯誤的輸入(如非法數據,異常流程,非業務允許輸入等)得到預期的錯誤結果
  • 邊界條件和異常
    • 空值,特殊取值,特殊時間點,數據順序,循環邊界等

單測覆蓋率

  • 行覆蓋

    • 統計單測執行了多少行
  • 分支覆蓋

    • 統計邏輯分支是否都覆蓋
  • 條件判定覆蓋

    • 所有的條件每種可能都執行一次,同時每種條件的結果也都至少執行一次

好用的測試框架

Junit5

常用注解

注解 說明
@Test 表示方法是測試方法,JUnit5中去掉了該注解的timeout參數支持
@BeforeEach 在每一個測試方法運行前,都運行一個指定的方法
@AfterEach 表示在每個單元測試之后執行
@BeforeAll 表示在所有單元測試之前執行
@AfterAll 表示在所有單元測試之后執行
@Tag 表示單元測試類別
@Disabled 表示測試類或測試方法不執行,類似於JUnit4中的@Ignore
@Timeout 表示測試方法運行如果超過了指定時間將會返回錯誤
@ExtendWith 為測試類或測試方法提供擴展類引用
@DisplayName 為測試類或者測試方法設置展示名稱
@RepeatedTest 表示方法可重復執行
@ParameterizedTest **:*表示方法是參數化測試

詳細注解:https://junit.org/junit5/docs/current/user-guide/

示例

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.*;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

class SortTest {

    @BeforeEach
    void init() {
        System.out.println("create connections...");
    }

    @AfterEach
    void destroy() {
        System.out.println("close connections...");
    }

    @Test
    @Tag("fast")
    @DisplayName("測試插入排序")
    @RepeatedTest(5)
    void insertSort() {
        int[] arr = {3, 1, 2, 5, 4};
        int[] result = Sort.insertSort(arr);
        Arrays.sort(arr);
        Assertions.assertThat(result)
                .isSorted()
                .as("測試插入排序");
    }

    @Test
    @Tag("fast")
    @DisplayName("測試插入排序--輸入為null")
    void insertSortNullArr() {
        int[] arr = null;
        int[] result = Sort.insertSort(arr);

        Assertions.assertThat(result)
                .isEqualTo(null)
                .as("測試插入排序,輸入為null,輸出為null");
    }

    @Test
    @DisplayName("過期的測試")
    @Disabled
    void insertSortDisabled() {
        //我過期了
    }

    @Test
    @DisplayName("超時測試")
    @Timeout(value = 1, unit = TimeUnit.SECONDS)
    public void timeoutTest() throws InterruptedException {
        TimeUnit.SECONDS.sleep((long) 0.9);
        //如果測試方法時間超過1s將會異常
//        TimeUnit.SECONDS.sleep((long) 1.9);
    }
}

todo Junit5測試SpringBoot

Mockito3

示例

  • 一版流程:given-when-then
@Test
public void thenReturn() {    
   //mock一個List類    
    List<String> list = mock(List.class);    
    //預設當list調用get()時返回hello,world    
    Mockito.when(list.get(anyInt())).thenReturn("hello,world");    
    String result = list.get(0);    
    Assert.assertEquals("hello,world", result);
}
  • 驗證行為是否發生
//模擬創建一個List對象
List<Integer> mock =  Mockito.mock(List.class);
//調用mock對象的方法
mock.add(1);
mock.clear();
//驗證方法是否執行
Mockito.verify(mock).add(1);
Mockito.verify(mock).clear();
  • 多次觸發返回不同值
//mock一個Iterator類
List<String> list = mock(List.class);
//預設當list調用get()時第一次返回hello,第n次都返回world
Mockito.when(list.get(anyInt())).thenReturn("hello").thenReturn("world");
// 下面這句和上面的語句等價
Mockito.when(list.get(anyInt())).thenReturn("hello","world");


//使用mock的對象
String result = list.get(0) + " " + list.get(1) + " " + list.get(2);
//驗證結果
Assert.assertEquals("hello world world",result);
  • 模擬拋出異常
@Test(expected = IOException.class)//期望報IO異常
public void when_thenThrow() throws IOException{    
    OutputStream mock = Mockito.mock(OutputStream.class);    
    //預設當流關閉時拋出異常    
    Mockito.doThrow(new IOException()).when(mock).close();    
    mock.close();
}
  • 匹配任意參數

Mockito.anyInt() 任何 int 值 ;
Mockito.anyLong() 任何 long 值 ;
Mockito.anyString() 任何 String 值 ;

Mockito.eq(Object) 任何匹配值 ;

Mockito.any(XXX.class) 任何 XXX 類型的值 ;

Mockito.any() 任何對象 等

List list = Mockito.mock(List.class);
//匹配任意參數
Mockito.when(list.get(Mockito.anyInt())).thenReturn(1);
Assert.assertEquals(1,list.get(1));Assert.assertEquals(1,list.get(999));

注意:使用了參數匹配,那么所有的參數都必須通過matchers來匹配

@Test
public void matchers() {    
    Comparator comparator = mock(Comparator.class);   
    comparator.compare("nihao", "hello");    
    //如果你使用了參數匹配,那么所有的參數都必須通過matchers來匹配    
    Mockito.verify(comparator).compare(anyString(), Mockito.eq("hello"));    
    //下面的為無效的參數匹配使用    
    // Mockito.verify(comparator).compare(anyString(),"hello");
}
  • void方法
@Test
public void Test() {    
	A a = Mockito.mock(A.class);    
    //void 方法才能調用doNothing()    
    Mockito.doNothing().when(a).setName("bb");   
    a.setName("bb");   
    // 斷言失敗    
    Assert.assertEquals("bb",a.getName());
}
class A {    
    private String name;    
    public void setName(String name){
    this.name = name;    
    }    
    public String getName(){        
        return name;    
    }
}
  • 預期回調接口生成期望值
@Test
public void answerTest(){
      List mockList = Mockito.mock(List.class);
      //使用方法預期回調接口生成期望值(Answer結構)
      Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new CustomAnswer());
      Assert.assertEquals("hello world:0",mockList.get(0));
      Assert.assertEquals("hello world:999",mockList.get(999));
  }
  private class CustomAnswer implements Answer<String> {
      @Override
      public String answer(InvocationOnMock invocation) throws Throwable {
          Object[] args = invocation.getArguments();
          return "hello world:"+args[0];
      }
  }
等價於:(也可使用匿名內部類實現)
@Test
public void answer_with_callback() {
    //使用Answer來生成我們我們期望的返回
    Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new Answer<Object>() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            return "hello world:"+args[0];
        }
    });
    Assert.assertEquals("hello world:0",mockList.get(0));
    Assert.assertEquals("hello world:999",mockList.get(999));
}

AssertJ

示例

其他

  • 注意bytebuddy的版本,mockito3.3.3 依賴 bytebuddy1.10.5
<!-- 單元測試 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.10.5</version>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.8.0</version>
    <scope>test</scope>
</dependency>
<!--   單測結束     -->
  • maven執行測試用例的插件,保證流水線執行測試:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M3</version>
    <configuration>
        <excludes>
            <exclude>some test to exclude here</exclude>
        </excludes>
    </configuration>
</plugin>
  • 集成JaCoCo代碼測試覆蓋率統計

個人理解

  • UT代碼要簡單;
  • 善用Mock;
  • UT的命名和注釋很重要;
  • UT並不能保證完全沒bug;
  • 如果發現單元測試不好寫,首先懷疑是代碼有問題,優先選擇重構代碼,實在不行再考慮使用框架的高級特性(如PowerMock可以mock私有方法,static方法,new變量等,這些特性理論上增加了寫垃圾代碼的可能 );
  • UT是體力話,寫UT時間和代碼開發時間相當;
  • UT是一件磨刀不誤砍柴工的事,長遠收益。故障越早發現,修復成本越低;
  • UT會倒逼開發人員白盒地思考代碼邏輯和結構,更好地對代碼進行設計,我認為這也是UT最重要的意義
  • 重構的時候心里有點底(我認為代碼應該隨時重構,哪怕一個變量名)
  • 寫單測是為了證明程序有錯,而不是程序無錯

后續

基於內存數據庫的單元測試...

參考

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html Mockito官方文檔

https://junit.org/junit5/docs/current/user-guide/ JUnit5 User Guide

https://time.geekbang.org/column/article/185684 極客時間-對於單元測試和重構的理解

https://www.liaoxuefeng.com/wiki/1252599548343744/1304065789132833

https://www.jianshu.com/p/ecbd7b5a2021


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM