進大廠必須要會的單元測試

2022-11-22 15:00:31

本文將按照如下順序給大家簡單講講單元測試應該怎麼寫

什麼是單元測試

單元測試又稱模組測試,是針對軟體設計的最小單位(模組)就行正確性的校驗的測試,檢查每個程式模組是否實現了規定的功能,保證其正常工作。

測試的重點:系統模組、方法的邏輯正確性

和整合測試不同,單元測試應該具備如下特點:

  1. 儘可能簡短不重複
  2. 執行速度快,因為單元測試幾乎可以一直執行,所以對於一些資料庫、檔案操作等一定要加快速度,可以採用mock的方式
  3. 具有100%的確定性,不能某幾次可以執行成功,某幾次執行失敗

我們在企業開發中,很多大公司都是要求單測到達一定的比率才能提交程式碼,單測能夠保證我們寫的邏輯程式碼符合我們的預期,並且在後續的維護中都能通過單測來驗證我們的修改有沒有把原有的程式碼邏輯改錯。

雖然會花費我們額外10%的時間去做單測,但是收益率還是值得的,作為一個開發,我認為我們本就該進行完整的自測後才移交給測試同學。

單元測試入門

先寫一個簡單的單測例子:測試一個求兩個set集合交集的方法

  1. 引入依賴
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.3.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.2</version>
    <scope>test</scope>
</dependency>
  1. 被測試方法
/**
     * 獲取交集
     * @param set1
     * @param set2
     * @return
     */
    public Set<Integer> getIntersection(Set<Integer> set1,Set<Integer> set2){
        set1.retainAll(set2);
        return set2;
    }
  1. 生成測試方法

我們可以通過IDEA的自動生成功能來生成測試方法

它會在test目錄下的同包名下生成一個測試類

  1. 我們編寫測試邏輯
class HelloServiceTest {

    @Test
    void getIntersection() {
    //生成mock類
        HelloService helloService = Mockito.mock(HelloService.class);
        //呼叫mock類的getIntersection方法時呼叫真實方法
        Mockito.when(helloService.getIntersection(Mockito.anySet(),Mockito.anySet())).thenCallRealMethod();

        Set<Integer> set1=new HashSet<>();
        set1.add(1);
        set1.add(2);
        set1.add(3);


        Set<Integer> set2=new HashSet<>();
        set2.add(5);
        set2.add(4);
        set2.add(3);

        Set<Integer> intersection = helloService.getIntersection(set1, set2);
        Set<Integer> set3=new HashSet<>();
        set3.add(3);
        //斷言,判斷方法結果是否和我們預想的一致
        Assertions.assertEquals(intersection,set3);
    }
}
  1. 執行

執行結果:

執行完後發現斷言異常,這樣就能檢查出我們之前寫的程式碼不對,去檢查了下,發現了問題,改正程式碼後重試。

 public Set<Integer> getIntersection(Set<Integer> set1,Set<Integer> set2){
        set1.retainAll(set2);
        return set1;
    }

常用方法

構建測試物件

  1. mock方法
  • 方法1
HelloService helloService = Mockito.mock(HelloService.class);
  • 方法2:
    使用註解
@Mock
private HelloService helloService;


@Test
void getIntersection() {
    //使用@Mock,需要加下面這行程式碼
    MockitoAnnotations.openMocks(this);
    Mockito.when(helloService.getIntersection(Mockito.anySet(),Mockito.anySet())).thenCallRealMethod();
    ...
    }

mock出來的物件,要指定方法的返回,否則只是返回預設值,不會執行真正的方法的實現。

  1. 直接使用new 方法構建物件
HelloService helloService = new HelloService();
  1. 使用@Spy註解
@Spy
private HelloService helloService;

使用@Spy註解的物件,在執行的時候會呼叫真實的方法。

上面都是簡單的一級物件的構建,如果被測試的物件裡面還要物件依賴怎麼辦呢?

構建依賴的測試物件

如這個方法:

@Setter
public class HelloService {


    private HelloDao helloDao;

    public String hello(){
        return helloDao.hello()+" xiaowang";
    }
    
}
  1. mock + set
HelloService helloService=new HelloService();
HelloDao helloDao = Mockito.mock(HelloDao.class);
helloService.setHelloDao(helloDao);
  1. @InjectMocks

使用@InjectMocks可以將mock出的依賴物件注入到它標註的測試物件中

    @InjectMocks
    private HelloService helloService;

    @Mock
    private HelloDao helloDao;

上面的例子中,將helloDao注入到了helloService中

構建靜態物件

需要修改依賴

<!--        <dependency>-->
<!--            <groupId>org.mockito</groupId>-->
<!--            <artifactId>mockito-core</artifactId>-->
<!--            <version>4.3.1</version>-->
<!--            <scope>test</scope>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>4.3.1</version>
            <scope>test</scope>
        </dependency>
MockedStatic<JsonUtils> tMockedStatic = Mockito.mockStatic(JsonUtils.class);

因為靜態方法mock了之後,在整個執行緒中都是生效的,如果需要隔離的話,可以使用try-with-resources來寫。

區別如下:

行為規定(打樁)

接下來我們學習方法的行為規定,因為mock出來的物件預設是不執行真實方法的,需要我們指定。

  1. doReturn
Mockito.doReturn("hello").when(helloDao).hello();
  1. thenReturn
Mockito.when(helloDao.hello()).thenReturn("hello");
  1. thenAnswer

這種方式可以靈活的返回,比如根據引數的不同返回不同的值

 Mockito.when(helloDao.hello(Mockito.anyString())).thenAnswer( invocation->{
            String param = invocation.getArgument(0);
            if(param.equals("w")){
                return "wang";
            }else {
                return "li";
            }
        });
  1. mock異常

有時候需要測試方法異常的時候對整個方法體的影響

Mockito.when(helloDao.hello(Mockito.anyString())).thenThrow(NullPointerException.class);

斷言

我們執行完測試方法後,就需要對結果進行驗證比對,來證明我們的方法的正確性。

  1. Assertions.assertEquals
Assertions.assertEquals(hello,"hello xiaowang");
  1. Assertions.assertTrue
Assertions.assertTrue(hello.equals("hello xiaowang"));
  1. Assertions.assertThrows

異常斷言,判斷是否是預期的異常

Assertions.assertThrows(NullPointerException.class,()->{
            helloDao.hello();
        });
  1. 使用Verify斷言執行次數
Mockito.verify(helloDao,Mockito.times(1)).hello();

番外

另外還有兩個註解,@BeforeEach和@AfterEach,顧名思義,一個是在test方法執行前執行,一個是在test方法執行後執行。

@BeforeEach
public void before(){
   System.out.println("before");
}

@AfterEach
public void after(){
   System.out.println("after");
}

另外推薦兩款比較好用的單測生成外掛 TestMe 和Diffblue