JUnit 5 單元測試教學

2022-11-18 12:01:59

點贊再看,動力無限。 微信搜「 程式猿阿朗 」。

本文 Github.com/niumoo/JavaNotes未讀程式碼部落格 已經收錄,有很多知識點和系列文章。

在軟體開發過程中,我們通常都需要測試自己的程式碼執行是否正常,可能對一個函數進行簡單測試,也可能是多個功能的組合測試。不管使用哪種方式,都是為了更好的測試我們的程式碼是否存在邏輯缺陷。測試對於軟體開發是非常必要的。

JUnit 5 介紹

在 Java 中比較有名的測試工具是 JUnit ,通常我們使用 JUnit 可以對一個邏輯單元進行測試,因此也叫單元測試。多個單元測試組合測試,可以確保我們的程式符合預期。JUnit 單元測試可以在開發階段發現問題,讓我們可以提前修復程式碼,因此十分重要。

JUnit 5 和 JUnit

JUnit 是一個 Java 語言的開源測試框架,使用 JUnit 讓我們使用註解就可以進行單元測試,很是方便。

JUnit 5 是 JUnit 的升級版本,JUnit 5 使用了 Java 8 及更高版本的 Java 語言特性,如函數程式設計,流式編碼等,因此更加強大。JUnit 5 進行單元測試的可讀性更強,編寫更加容易,且可以輕鬆擴充套件。

JUnit 5 基本元件

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform

JUnit Platform 是 JUnit 的基礎框架,使用 JUnit Platform 才能在 JVM 啟動測試,JUnit Platform 還定義了 TestEngine 測試引擎,是JUnit 測試的基礎。

JUnit Jupiter

JUnit Jupiter 提供了單元測試常見的註解以及擴充套件介面,想要方便的進行 JUnit 單元測試,那麼 Jupiter 模組就必不可少。

JUnit Vintage

JUnit Vintage 提供了對 JUnit 3 和 JUnit 4 的測試支援。

JUnit 5 依賴

使用註解進行 JUnit 單元測試,直接引入 junit-jupiter即可。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
  	<version>5.9.1</version>
    <scope>test</scope>
</dependency>

JUnit 5 常用註解

@Test

為一個 public void 方法新增 @Test 註釋,允許我們對這個方法進行測試。

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/** 
 * @author:https://www.wdbyte.com  
 **/
class JUnitTestIsDog {

    @Test
    public void testIsDog() {
        String name = "cat";
        Assertions.assertEquals(name, "dog");
    }
}

上面的程式碼中使用了 Assertions.assertEquals(name, "dog") 來判斷是否 name 變數是否是 dogAssertionsJUnit 提供的斷言工具,後面會詳細介紹。

idea 中執行可以到的錯誤紀錄檔,提示預期是 dog,實際是 cat

org.opentest4j.AssertionFailedError: 
Expected :cat
Actual   :dog
<Click to see difference>

如果是符合預期的,那麼執行會顯示正確標誌。

@Test
public void testIsDog2() {
    String name = "dog";
    Assertions.assertEquals(name, "dog");
}

testIsDog2 方法測試通過。

@BeforeAll

使用 @BeforeAll 可以在單元測試前初始化部分資訊,@BeforeAll 只能使用在靜態方法上,被註解的方法會在測試開始前執行一次

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** 
 * @author:https://www.wdbyte.com  
 **/
class JUnitBeforeAll {

    @BeforeAll
    public static void init() {
        System.out.println("初始化,準備測試資訊");
    }

    @Test
    public void testIsDog() {
        String name = "dog";
        Assertions.assertEquals(name, "dog");
        System.out.println("is dog");
    }

    @Test
    public void testIsCat() {
        String name = "cat";
        Assertions.assertEquals(name, "cat");
        System.out.println("is cat");
    }
}

這會輸出:

初始化,準備測試資訊
is cat
is dog

@BeforeEach

使用 @BeforeEach 註解的方法,會在每一個 @Test 註解的方法執行前執行一次。

class JUnitBeforeAll {

    @BeforeAll
    public static void init() {
        System.out.println("初始化,準備測試資訊");
    }

    @BeforeEach
    public void start(){
        System.out.println("開始測試...");
    }

    @Test
    public void testIsDog() {
        String name = "dog";
        Assertions.assertEquals(name, "dog");
        System.out.println("is dog");
    }

    @Test
    public void testIsCat() {
        String name = "cat";
        Assertions.assertEquals(name, "cat");
        System.out.println("is cat");
    }
}

這會輸出:

初始化,準備測試資訊
開始測試...
is cat
開始測試...
is dog

@AfterAll

@AfterAll 註解只能使用在靜態方法上,被註解的方法會在所有單元測試執行完畢後執行一次。

class JUnitBeforeAll {

    @BeforeAll
    public static void init() {
        System.out.println("初始化,準備測試資訊");
    }

    @BeforeEach
    public void start(){
        System.out.println("開始測試...");
    }

    @Test
    public void testIsDog() {
       //...
    }

    @Test
    public void testIsCat() {
        //...
    }

    @AfterAll
    public static void close() {
        System.out.println("結束,準備退出測試");
    }
}

這會輸出:

初始化,準備測試資訊
開始測試...
is cat
開始測試...
is dog
結束,準備退出測試

@AfterEach

使用 @AfterEach 註解的方法,會在每一個 @Test 註解的方法執行結束前執行一次

class JUnitBeforeAll {

    @BeforeAll
    public static void init() {
        System.out.println("初始化,準備測試資訊");
    }

    @BeforeEach
    public void start(){
        System.out.println("開始測試...");
    }

    @Test
    public void testIsDog() { //... }

    @Test
    public void testIsCat() { //... }

    @AfterEach
    public void end(){
        System.out.println("測試完畢...");
    }

    @AfterAll
    public static void close() {
        System.out.println("結束,準備退出測試");
    }
}

這會輸出:

初始化,準備測試資訊
開始測試...
is cat
測試完畢...
開始測試...
is dog
測試完畢...
結束,準備退出測試

@Disabled

@Disabled 註解的方法不在參與測試,下面對 testIsDog 方法新增了 @Disabled 註解。

class JUnitBeforeAll {

    @BeforeAll
    public static void init() {
        System.out.println("初始化,準備測試資訊");
    }

    @BeforeEach
    public void start(){
        System.out.println("開始測試...");
    }

    @Disabled("由於xx原因,關閉 testIsDog 測試")
    @Test
    public void testIsDog() {
        String name = "dog";
        Assertions.assertEquals(name, "dog");
        System.out.println("is dog");
    }

    @Test
    public void testIsCat() {
        String name = "cat";
        Assertions.assertEquals(name, "cat");
        System.out.println("is cat");
    }

    @AfterEach
    public void end(){
        System.out.println("測試完畢...");
    }

    @AfterAll
    public static void close() {
        System.out.println("結束,準備退出測試");
    }
}

這會輸出:

初始化,準備測試資訊
開始測試...
is cat
測試完畢...

由於xx原因,關閉 testIsDog 測試
結束,準備退出測試

@DisplayName

使用 @DisplayName 註解可以自定義測試方法的顯示名稱,下面為兩個測試方法自定義名稱。

class JUnitBeforeAll {

    @BeforeAll
    public static void init() {
        System.out.println("初始化,準備測試資訊");
    }

    @BeforeEach
    public void start() {
        System.out.println("開始測試...");
    }

    @DisplayName("是否是狗")
    @Disabled
    @Test
    public void testIsDog() {
        String name = "dog";
        Assertions.assertEquals(name, "dog");
        System.out.println("is dog");
    }

    @DisplayName("是否是貓")
    @Test
    public void testIsCat() {
        String name = "cat";
        Assertions.assertEquals(name, "cat");
        System.out.println("is cat");
    }

    @AfterEach
    public void end() {
        System.out.println("測試完畢...");
    }

    @AfterAll
    public static void close() {
        System.out.println("結束,準備退出測試");
    }
}

idea 中執行後,可以看到設定的中文名稱。

@ParameterizedTest

使用註解 @ParameterizedTest 結合 @ValueSource ,可以對不用的入參進行測試。下面的範例使用 @ParameterizedTest 來開始引數化單元測試,name 屬性用來定義測試名稱, @ValueSource 則定義了兩個測試值。

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class JUnitParam {

    //@Test
    @DisplayName("是否是狗")
    @ValueSource(strings = {"dog", "cat"})
    @ParameterizedTest(name = "開始測試入參 {0} ")
    public void testIsDog(String name) {
        Assertions.assertEquals(name, "dog");
    }
}

這會輸出:

@Order

在類上增加註解 @TestMethodOrder ,然後在方法上使用 @Order 指定順序,數位越小優先順序越搞,可以保證測試方法執行順序。

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledOnJre;

import static org.junit.jupiter.api.condition.JRE.JAVA_19;

@TestMethodOrder(OrderAnnotation.class)
public class JUnitOrder{

    @Test
    @DisplayName("測試是否是狗")
    @Order(2)
    public void testIsDog() {
        String name = "dog";
        Assertions.assertEquals(name, "dog");
        System.out.println("is dog");
    }

    @DisplayName("是否是貓")
    @Test
    @Order(1)
    public void testIsCat() {
        String name = "cat";
        Assertions.assertEquals(name, "cat");
        System.out.println("is cat");
    }
}

這會輸出:

is cat
is dog

其他註解

@EnabledOnJre(JAVA_19)

只在 JRE 19 環境執行,否則執行會輸出:Disabled on JRE version: xxx.

@RepeatedTest(10)

重複測試,引數 10 可以讓單元測試重複執行 10 次。

JUnit 5 常用斷言

在上面的例子中,已經用到了 assertEquals 來判斷結果是否符合預期,assertEquals是類 org.junit.jupiter.api.Assertions 中的一個方法;除此之外,還幾乎包括了所有我們日常測試想要用到的判斷方法。

下面是一些演示:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class JunitAssert {

    @DisplayName("是否是狗")
    @Test
    public void testIsDog() {
        String name = "dog";
        Assertions.assertNotNull(name);
        Assertions.assertEquals(name, "dog");
        Assertions.assertNotEquals(name, "cat");
        Assertions.assertTrue("dog".equals(name));
        Assertions.assertFalse("cat".equals(name));
    }

    @DisplayName("是否是貓")
    @Test
    public void testIsCat() {
        String name = "cat";
        Assertions.assertNull(name, "name is not null");
    }

}

testIsDog 中演示了一些常用的判斷方法,且都可以通過驗證。在 testIsCat 方法中進行了 null 值判斷,顯然這裡無法通過測試,會丟擲自定義異常 name is not null

這會輸出:

org.opentest4j.AssertionFailedError: name is not null ==> 
Expected :null
Actual   :cat
<Click to see difference>

預期是一個 null 值,實際上是一個 cat 字串。

Maven JUnit 測試

在 Maven 中進行 JUnit 測試,可以通過命令 mvn test 開始測試,預設情況下會測試所有依賴了當前原始碼的 JUnit 測試用例。

準備被測 Preson類放在 src.main.java.com.wdbyte.test.junit5.

package com.wdbyte.test.junit5;

public class Person {
    public int getLuckyNumber() {
        return 7;
    }
}

編寫測試類 PersonTest 放在 src.test.java.com.wdbyte.test.junit5. 這裡判斷獲取到的幸運數位是否是 8 ,明顯方法返回的是 7 ,所以這裡是測試會報錯。

package com.wdbyte.test.junit5;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("測試 Presion")
class PersonTest {

    @DisplayName("測試幸運數位")
    @Test
    void getLuckyNumber() {
        Person person = new Person();
        Assertions.assertEquals(8, person.getLuckyNumber());
    }
}

在 pom.xml 中引入 maven junit 測試依賴外掛。

<build>
     <plugins>
         <plugin>
             <artifactId>maven-surefire-plugin</artifactId>
             <version>2.22.2</version>
         </plugin>
         <plugin>
             <artifactId>maven-failsafe-plugin</artifactId>
             <version>2.22.2</version>
         </plugin>
     </plugins>
</build>

執行測試命令:mvn test

➜  junit5-jupiter-starter git:(master) ✗ mvn test
[INFO] Scanning for projects...
[INFO] ....
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.wdbyte.test.junit5.PersonTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.031 s <<< FAILURE! - in com.wdbyte.test.junit5.PersonTest
[ERROR] getLuckyNumber  Time elapsed: 0.026 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <8> but was: <7>
	at com.wdbyte.test.junit5.PersonTest.getLuckyNumber(PersonTest.java:18)

[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR]   PersonTest.getLuckyNumber:18 expected: <8> but was: <7>
[INFO]
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.777 s
[INFO] Finished at: 2022-11-17T23:01:09+08:00
[INFO] ------------------------------------------------------------------------

也可以指定類進行測試:mvn -Dtest=PersonTest test

一如既往,文章中程式碼存放在 Github.com/niumoo/javaNotes.

<完>

文章持續更新,可以微信搜一搜「 程式猿阿朗 」或存取「程式猿阿朗部落格 」第一時間閱讀。本文 Github.com/niumoo/JavaNotes 已經收錄,有很多知識點和系列文章,歡迎Star。