Junit5




JUnit5 介紹

什么是 xUnit ?

image

Java 語⾔的 xUnit 主流框架:

image

什么是 JUnit5 ?

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform::用於 JVM 上啟動測試框架的基礎服務,提供命令行,IDE和構建工具等方式執行測試的支持。不僅支持 Junit 自制的測試引擎,其他測試引擎也都可以接入。
  • JUnit Jupiter:JUnit Jupiter 提供了 JUnit5 的新的編程模型和擴展模型,是 JUnit5 新特性的核心。內部包含了一個測試引擎,用於在 Junit Platform 上運行。
  • JUnit Vintage:由於 JUint 已經發展多年,為了照顧老的項目,JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的測試引擎。

image

通過上述的介紹,不知道有沒有發現 JUint5 似乎已經不再滿足於安安靜靜做一個單元測試框架了,它的野心很大,想通過接入不同測試引擎,來支持各類測試框架的使用,成為一個基於 JVM 測試框架的基礎平台。因此它也采用了分層的架構,分為了平台層、引擎層、框架層。下圖可以很清晰地體現出來:

image

  • 最核心的就是平台層:IDE 和構建工具都是作為客戶端和這個平台層交互,以達到在項目中運行測試的目的。TestEngine 的實現在平台層中用於發現和運行測試,並且輸出測試報告,並通過平台層返回給客戶端。

  • 核心關注點是擴展能力:不僅僅只是存在於測試類級別,在整個測試平台級別,都提供了足夠的擴展能力。只需要實現框架本身對 TestEngine 的接口,任何測試框架都可以在 JUnit Platform 上運行,這代表着 JUnit5 將會有着很強的拓展性。只需要一點點工作,通過這一個擴展點,框架就能得到所有 IDE 和構建工具在測試上的支持。這對於新框架來說絕對是好事,在測試和構建這塊的門檻更低。如 JUnit Vintage 就是一個 TestEngine 實現,用於執行 JUnit4 的測試。

  • 這些對於一個開發者來說意味着什么呢?這意味着一個測試框架和 JVM 開發市場上所有主流的工具集成的時候,你能更容易地說服你的經理、開發 leader、任何項阻礙你引入這個測試框架的人。

Junit5 新特性

JUnit5 更像是 JUnit4 的一個超集,他提供了非常多的增強:

  • JUnit5 不再是單個庫,而是模塊化結構的集合。整個 API 分成了:自己的模塊、引擎、Launcher、針對 Gradle 和 Surefire 的集成模塊。
  • 更強大的斷言功能和測試注解。
  • 嵌套測試類:不僅僅是 BDD(Behavior Driven Development)。
  • 動態測試:在運行時生成測試用例。
  • 擴展測試:JUnit5 提供了很多的標准擴展接口,第三方可以直接實現這些接口來提供自定義的行為。通過 @ExtendWith 注解可以聲明在測試方法和類的執行中啟用相應的擴展。
  • 支持 Hamcrest 匹配和 AssertJ 斷言庫,可以用它們來代替 JUnit5 的方法。
  • 實現了模塊化,讓測試執行和測試發現等不同模塊解耦,減少依賴。
  • 提供對 Java8 的支持,如 Sream API、Lambda 表達式(允許你通過表達式來代替功能接口)等。
  • 提供了分組斷言(允許執行一組斷言,且會一起報告)。

遷移指南:

JUnit 平台可以通過 Jupiter 引擎來運行 JUnit 5 測試,通過 Vintage 引擎來運行 JUnit 3 和 JUnit 4 測試。因此,已有的 JUnit 3 和 4 的測試不需要任何修改就可以直接在 JUnit 平台上運行。只需要確保 Vintage 引擎的 jar 包出現在 classpath 中,JUnit 平台會自動發現並使用該引擎來運行 JUnit 3 和 4 測試。

開發人員可以按照自己的項目安排來規划遷移到 JUnit 5 的進度。可以保持已有的 JUnit 3 和 4 的測試用例不變,而新增加的測試用例則使用 JUnit 5。

在進行遷移的時候需要注意如下的變化:

  • 注解在 org.junit.jupiter.api 包中;斷言在 org.junit.jupiter.api.Assertions 類中;前置條件在 org.junit.jupiter.api.Assumptions 類中
  • 把 @Before 和 @After 替換成 @BeforeEach 和 @AfterEach
  • 把 @BeforeClass 和 @AfterClass 替換成 @BeforeAll 和 @AfterAll
  • 把 @Ignore 替換成 @Disabled
  • 把 @Category 替換成 @Tag
  • 把 @RunWith、@Rule 和 @ClassRule 替換成 @ExtendWith

Junit5 注解

注解 說明
@Test 表示方法是測試方法(與 JUnit4 的 @Test 不同,它的職責非常單一,不能聲明任何屬性,拓展的測試將會由 Jupiter 提供額外注解)
@ParameterizedTest 表示方法是參數化測試
@RepeatedTest 表示方法可重復執行
@DisplayName 為測試類或者測試方法設置展示名稱
@BeforeEach 表示在每個測試方法之前執行
@AfterEach 表示在每個測試方法之后執行
@BeforeAll 只執行一次,執行時機是在所有測試方法和 @BeforeEach 注解方法之前
@AfterAll 只執行一次,執行時機是在所有測試方法和 @AfterEach 注解方法之后
@Tag 表示單元測試類別。類似於 JUnit4 中的 @Categories
@Disabled 表示測試類或測試方法不執行。類似於 JUnit4 中的 @Ignore
@Timeout 表示測試方法運行如果超過了指定時間將會返回錯誤
@ExtendWith 為測試類或測試方法提供擴展類引用
  • JUnit5 不再需要手動將測試類與測試方法為 public,包可見的訪問級別就足夠了。
  • 因為框架會為每個測試類創建一個單獨的實例,且在 @BeforeAll/@AfterAll 方法執行時,尚無任何測試實例誕生。因此,這兩個方法必須定義為靜態方法。

示例:

  • Maven 依賴
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>
  • 測試代碼
import org.junit.jupiter.api.*;

class StandardTests {

    @BeforeAll
    static void initAll() {
        System.out.println("BeforeAll");
    }

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

    @Test
    void test1() {
        System.out.println("test1");
    }

    @Test
    void test2() {
        System.out.println("test2");
    }

    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
        System.out.println("skippedTest");
    }

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

    @AfterAll
    static void tearDownAll() {
        System.out.println("AfterAll");
    }

}

執行結果:

BeforeAll
BeforeEach
test1
AfterEach
BeforeEach
test2
AfterEach

for demonstration purposes
AfterAll

斷言

JUnit5 使用了新的斷言類:org.junit.jupiter.api.Assertions。相比之前的 Assert 斷言類多了許多新的功能,並且大量方法支持 Java8 的 Lambda 表達式。

JUnit5 常用斷言方法:

方法 說明
assertEquals(expected, actual) 查看兩個對象是否相等。
(類似於字符串比較使用的 equals() 方法)
assertNotEquals(first, second) 查看兩個對象是否不相等。
assertNull(object) 查看對象是否為空。
assertNotNull(object) 查看對象是否不為空。
assertSame(expected, actual) 查看兩個對象的引用是否相等。
(類似於使用“==”比較兩個對象)
assertNotSame(unexpected, actual) 查看兩個對象的引用是否不相等。
(類似於使用“!=”比較兩個對象)
assertTrue(condition) 查看運行結果是否為 true。
assertFalse(condition) 查看運行結果是否為 false。
assertArrayEquals(expecteds, actuals) 查看兩個數組是否相等。
assertThat(actual, matcher) 查看實際值是否滿足指定的條件。
fail() 讓測試執行失敗。

以下為兩個與 JUnit4 不太一樣的斷言方式。

異常斷言

JUnit5 提供了一種新的異常斷言方式 Assertions.assertThrows(),配合函數式編程就可以進行使用。

我們先來考慮一下下面這個 JUnit4 測試:

@Test(expected = IllegalArgumentException.class)
public void shouldThrowException() throws Exception {
    Task task = buildTask();
    LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
    task.execute(oneHourAgo);
}

想象我們運行這個測試,如果傳入到 execute() 方法中的參數是一個過去的時間,會正常拋出一個 IllegalArgumentException 異常。這種情況下,測試會運行通過。

但是如果在 buildTask() 方法中拋出了一個其他類型的異常呢?測試會正常執行,並且會提示你得到的異常和期望異常不匹配。這里問題就出來了,我們只是希望測試是在指定位置得到指定的異常,而不是在整個測試體中出現的異常都作為對比異常。

為此,在 JUnit5 中,提供了一個 assertThrows() 方法,可以非常輕松地處理這個問題:

@Test
void shouldThrowException() throws Exception {
    Task task = buildTask();
    LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
    assertThrows(IllegalArgumentException.class,
                 () -> task.execute(oneHourAgo));
}

超時斷言

同樣的,Junit5 還提供了 Assertions.assertTimeout() 方法,為測試方法的指定位置,設置超時測試。

在這種情況下,就不會擔心測試的 setup 階段對代碼執行時間的影響,你可以指定只去衡量某一段代碼的執行時間。

另外還提供了一個選項:當出現超時的時候,是選擇停止執行(即斷言失敗)還是繼續當前測試(以衡量代碼執行的真實完整時間)。

@Test
void shouldTimeout() throws Exception {
    ExpensiveService service = setupService();
    assertTimeout(ofSeconds(3), () -> {
        service.expensiveMethod();
    });
}

AssertAll(軟斷言)

問題現象:有⼀個⽅法存在多個斷⾔,但是其中⼀個斷⾔失敗了,后⾯的斷⾔都沒有執⾏,難道我要等第⼀個問題修好了才能繼續檢查后⾯的斷⾔么?

問題原因:因為原來使⽤的是 JUnit5 的普通斷⾔,當⼀個斷⾔失敗會直接跳出測試⽅法,導致后⾯的斷⾔⽆法執⾏,此時的腳本容錯性較低。

解決思路

  • 拆開多個測試⽅法,每個測試⽅法進⾏⼀個斷⾔。(會造成⼤量重復代碼,此⽅案被否)
  • 使⽤軟斷⾔,即使⼀個斷⾔失敗,仍會進⾏進⾏余下的斷⾔,然后統⼀輸出所有斷⾔結果。

實施⽅案:可以使⽤ JUnit5 提供的 Java8lambdas的斷⾔⽅法,當⼀個斷⾔失敗,剩下的斷⾔依然會執⾏,腳本的容錯性增強。

Junit5 帶來新的斷⾔⽅式assertAll 斷⾔⽅法,會在執⾏完所有斷⾔后統⼀輸出結果,⼀次性暴露所有問題,提⾼了測試腳本的健壯性。

    @Test
    public void fun() {
        // 組內有一個斷言方法不通過,則整組斷言結果也不通過
        assertAll("斷言描述1",
                () -> assertEquals(1, 2),  // 斷言失敗
                () -> System.out.println("測試打印")  // 即使上一行斷言失敗,仍能執行該行代碼
        );

        // 若上組斷言不通過,則該組斷言不執行
        assertAll("斷言描述2",
                () -> assertEquals(1, 2),
                () -> assertEquals(2, 2)
        );
    }

運行結果:

測試打印

expected: <1> but was: <2>
Comparison Failure: 
Expected :1
Actual   :2
<Click to see difference>



org.opentest4j.MultipleFailuresError: 斷言描述1 (1 failure)
    org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
.....

綜合示例

class AssertionsDemo {

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "The optional assertion message is now the last parameter.");
        assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and any
        // failures will be reported together.
        assertAll("person",
            () -> assertEquals("John", person.getFirstName()),
            () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        // Within a code block, if an assertion fails the
        // subsequent code in the same block will be skipped.
        assertAll("properties",
            () -> {
                String firstName = person.getFirstName();
                assertNotNull(firstName);

                // Executed only if the previous assertion is valid.
                assertAll("first name",
                    () -> assertTrue(firstName.startsWith("J")),
                    () -> assertTrue(firstName.endsWith("n"))
                );
            },
            () -> {
                // Grouped assertion, so processed independently
                // of results of first name assertions.
                String lastName = person.getLastName();
                assertNotNull(lastName);

                // Executed only if the previous assertion is valid.
                assertAll("last name",
                    () -> assertTrue(lastName.startsWith("D")),
                    () -> assertTrue(lastName.endsWith("e"))
                );
            }
        );
    }

    @Test
    void exceptionTesting() {
        Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", exception.getMessage());
    }

    @Test
    void timeoutNotExceeded() {
        // The following assertion succeeds.
        assertTimeout(ofMinutes(2), () -> {
            // Perform task that takes less than 2 minutes.
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        // The following assertion succeeds, and returns the supplied object.
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod() {
        // The following assertion invokes a method reference and returns an object.
        String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
        assertEquals("hello world!", actualGreeting);
    }

    @Test
    void timeoutExceeded() {
        // The following assertion fails with an error message similar to:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    private static String greeting() {
        return "hello world!";
    }

}

參數化測試

在 JUnit4 中,如果想要實現參數化測試(使用不同的參數來測試相同的一個方法),只能使用測試類中的字段來實現。而在 JUnit5 中,提供了參數化測試來實現這個需求。不同的參數值可以直接和一個測試方法關聯,並且允許直接在一個測試類中提供不同的參數值直接參與測試,這些在 JUnit4 中都是無法實現的。

JUnit5 的參數化可以通過一組 CSV 格式的字符串、外部的 CSV、YML、JSON 文件、枚舉、工廠方法,或者指定的提供類來提供。CSV 中的字符串類型的值還可以自動地轉化為指定的類型,並且可以完成自己的類型轉換器,如將 String 轉成你希望的任何指定類型。

  • @ValueSource:指定入參來源,支持八大基礎類、String、Class 類型
  • @NullSource:提供一個 null 入參
  • @EnumSource:提供一個枚舉入參
  • @MethodSource:通過一個方法入參(該方法實現了自定義的數據獲取方式)
  • @CsvSource:提供 CSV 格式的入參
  • @CsvFileSource:通過 CSV 文件提供參數
  • @ArgumentsSource

簡單參數與 CSV 參數

測試類:

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class ParameterTest {

    @DisplayName("Parameter Count : 1")
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    void test1(int num1) {
        assertTrue(num1 < 4);
    }

    @DisplayName("Parameter Count : 2")
    @ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}")
    @CsvSource({
            "apple, 1",
            "banana, 2"
    })
    void test2(String fruit, int qty) {
        assertTrue(true);
    }

    @DisplayName("Parameter Count : 3")
    @ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}, price={2}")
    @CsvSource({
            "apple, 1, 1.99",
            "banana, 2, 2.99"
    })
    void test3(String fruit, int qty, BigDecimal price) {
        assertTrue(true);
    }

    /**
     * csv文件內容:
     * name, age
     * shawn, 24
     */
    @DisplayName("參數化測試-從csv文件獲取")
    @ParameterizedTest
    @CsvFileSource(resources="/test.csv", numLinesToSkip=1)  // 指定csv文件位置,並忽略標題行
    public void parameterizedTestWithCsv(String name, Integer age) {
        System.out.println("name:" + name + ", age:" + age);
        Assertions.assertNotNull(name);
        Assertions.assertNotNull(age);
    }

}

執行結果:

image

Json 數據驅動

測試類:

import com.bean.User;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.util.List;

public class ParameterTest {

    /**
     * Json文件內容:
     * [
     *   {"name": "apple", "age": "12"},
     *   {"name": "banana", "age": "13"}
     * ]
     */
    static List<User> testDDTFromJson() throws IOException {

        ObjectMapper objectMapper = new ObjectMapper();
        TypeReference typeReference = new TypeReference<List<User>>(){};

        List<User> users = (List<User>) objectMapper.readValue(
                ParameterTest.class.getResourceAsStream("/user.json"),  // 本類名反射
                typeReference
        );

        return users;
    }

    @ParameterizedTest
    // @MethodSource("testDDTFromJson")  // 指定獲取數據源的方法名
    @MethodSource  // 若不指定方法名,則自動找同名方法
    @DisplayName("從方法獲取測試數據")
     void testDDTFromJson(User user) {
        System.out.println(user);
        Assertions.assertTrue(user.name.length() > 3);
    }

}

Yaml 數據驅動

Yaml 相關依賴:

 <dependency>
   <groupId>com.fasterxml.jackson.dataformat</groupId>
   <artifactId>jackson-dataformat-yaml</artifactId>
   <version>2.3.3</version>
</dependency>
 <dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.3.3</version>
 </dependency>

測試類:

import com.bean.User;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.util.List;

public class ParameterTest {

    /**
     * Yaml文件內容:
     * - name: apple
     *   age: 12
     * - name: banana
     *   age: 13
     */
    static List<User> testDDTFromYaml() throws IOException {

        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
        TypeReference typeReference = new TypeReference<List<User>>(){};
		
        List<User> users = (List<User>) objectMapper.readValue(
                ParameterTest.class.getResourceAsStream("/user.yaml"),  // 本類名反射
                typeReference
        );
		
        return users;
    }

    @ParameterizedTest
    // @MethodSource("testDDTFromYaml")  // 指定獲取數據源的方法名
    @MethodSource  // 若不指定方法名,則自動找同名方法
    @DisplayName("從方法獲取測試數據")
     void testDDTFromYaml(User user) {
        System.out.println(user);
        Assertions.assertTrue(user.name.length() > 3);
    }

}

嵌套測試

JUnit5 提供了嵌套測試用於更好表示各個單元測試類之間的關系。平時我們寫單元測試時一般都是一個類對應一個單元測試類,不過有些互相之間有業務關系的類,他們的單元測試完全是可以寫在一起。因此,使用內嵌的方式表示,能夠減少測試類的數量,防止類爆炸。

JUnit5 提供了 @Nested 注解,能夠以靜態成員內部類的形式對測試用例類進行邏輯分組。

示例:

import org.junit.jupiter.api.*;

class NestedTest {

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

    @Test
    @DisplayName("Nested")
    void test() {
        System.out.println("test");
    }

    @Nested
    @DisplayName("Nested2")
    class Nested2 {

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

        @Test
        void Nested2_test1() {
            System.out.println("Nested2_test1");
        }

        @Test
        void Nested2_test2() {
            System.out.println("Nested2_test2");
        }

        @Nested
        @DisplayName("Nested3")
        class Nested3 {

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

            @Test
            void Nested3_test1() {
                System.out.println("Nested3_test1");
            }

            @Test
            void Nested3_test2() {
                System.out.println("Nested3_test2");
            }
        }
    }

}

執行結果:

init
test
init
Nested2_init
Nested2_test1
init
Nested2_init
Nested2_test2
init
Nested2_init
Nested3_init
Nested3_test1
init
Nested2_init
Nested3_init
Nested3_test2

忽略測試

JUnit5 提供 @Disabled 禁用整個測試類或單個測試方法上的測試執行。

@Disabled("忽略執行的描述")
@Test
void Demo() {
    System.out.println("忽略執行");
}

重復測試

重復運行單元測試可以更加保證測試的准確性,規避一些隨機性帶來的測試問題。

@RepeatedTest(10)  // 表示重復執行10次
@DisplayName("重復測試")
public void testRepeated() {
    Assertions.assertTrue(1==1);
}

前置條件

JUnit5 中的前置條件(Assumptions)類似於斷言,不同之處在於不滿足的斷言會使得測試方法失敗,而不滿足的前置條件只會使得測試方法的執行終止。

前置條件可以看成是測試方法執行的前提,當該前提不滿足時,就沒有繼續執行的必要。在如下案例中:

  • assumeTrue 和 assumFalse 確保給定的條件為 true 或 false,不滿足條件會使得測試方法執行終止。
  • assumingThat 的參數是分別表示條件的布爾值和 Executable 接口的實現對象。只有條件滿足時,Executable 對象才會被執行;當條件不滿足時,測試方法的執行並不會終止。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Objects;

import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;

public class AssumeTest {

    private final String env = "dev1";

    @Test
    @DisplayName("assume simple")
    public void testSimpleAssume() {
        assumeTrue(Objects.equals(this.env, "dev1"));
        System.out.println("環境是 dev1");  // 執行輸出
        assumeTrue(Objects.equals(this.env, "dev2"));  // org.opentest4j.TestAbortedException: Assumption failed: assumption is not true
        System.out.println("環境是 dev2");  // 不執行輸出
    }

    @Test
    @DisplayName("assume then do")
    public void testAssumeThenDo() {
        assumingThat(  // 只有條件滿足時,Executable 對象才會被執行
                Objects.equals(this.env, "dev2"),  // 表示條件的布爾值
                () -> System.out.println("In dev1")  // Executable 接口的實現對象
        );
        // 即使上述不滿足條件,也會繼續執行剩下代碼
        System.out.println("始終執行");
    }

}

測試執行順序

以下展示如何通過 MethodOrderer 類控制 JUnit5 的測試執行順序。

Alphanumeric:字母數字順序

示例:

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import static org.junit.jupiter.api.Assertions.assertEquals;

@TestMethodOrder(MethodOrderer.Alphanumeric.class)
public class AlphanumericTest {

    @Test
    void testZ() {
        assertEquals(2, 1 + 1);
    }

    @Test
    void testA() {
        assertEquals(2, 1 + 1);
    }

    @Test
    void testY() {
        assertEquals(2, 1 + 1);
    }

    @Test
    void testE() {
        assertEquals(2, 1 + 1);
    }

    @Test
    void testB() {
        assertEquals(2, 1 + 1);
    }

}

執行結果:

testA()
testB()
testE()
testY()
testZ()

OrderAnnotation:根據 @Order 值排序

示例:

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderAnnotationrTest {
 
    @Test
    void test0() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    @Order(3)
    void test1() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    @Order(1)
    void test2() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    @Order(2)
    void test3() {
        assertEquals(2, 1 + 1);
    }
 
    @Test
    void test4() {
        assertEquals(2, 1 + 1);
    }
 
}

執行結果:

test2()
test3()
test1()
test0()
test4()

Random:隨機順序

示例:

@TestMethodOrder(MethodOrderer.Random.class)
class RandomTest {
 
    @Test
    void aTest() {}
 
    @Test
    void bTest() {}
 
    @Test
    void cTest() {}
}

還可配置自定義種子 junit.jupiter.execution.order.random.seed 以創建可重復的測試版本:

  • 方式一:在 junit-platform.properties 屬性文件中配置
junit.jupiter.execution.order.random.seed=99
  • 方式二:在 Maven 的 pom.xml 中配置參數
<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M3</version>
        <configuration>
            <properties>
                <configurationParameters>
                    junit.jupiter.execution.order.random.seed=99
                </configurationParameters>
            </properties>
        </configuration>
    </plugin>

實現自定義順序

示例:

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.math.BigDecimal;
import java.util.Comparator;

import static org.junit.jupiter.api.Assertions.assertTrue;

// 根據方法入參個數的順序執行
class ParameterCountOrder implements MethodOrderer {

    private Comparator<MethodDescriptor> comparator =
            Comparator.comparingInt(md1 -> md1.getMethod().getParameterCount());

    @Override
    public void orderMethods(MethodOrdererContext context) {
        context.getMethodDescriptors().sort(comparator.reversed());
    }

}

@TestMethodOrder(ParameterCountOrder.class)
public class MethodParameterCountTest {

    @DisplayName("Parameter Count : 2")
    @ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}")
    @CsvSource({
            "apple, 1",
            "banana, 2"
    })
    void test2(String fruit, int qty) {
        assertTrue(true);
    }

    @DisplayName("Parameter Count : 1")
    @ParameterizedTest(name = "{index} ==> ints={0}")
    @ValueSource(ints = {1, 2, 3})
    void test1(int num1) {
        assertTrue(num1 < 4);
    }

    @DisplayName("Parameter Count : 3")
    @ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}, price={2}")
    @CsvSource({
            "apple, 1, 1.99",
            "banana, 2, 2.99"
    })
    void test3(String fruit, int qty, BigDecimal price) {
        assertTrue(true);
    }

}

執行結果:

image


動態測試

問題場景:運維團隊有⼀批做線上配置檢查腳本,領導希望將他們的測試結果整合到我們的 Junit 測試報告⾥,他們有⼏千條測試⽤例,⽽且是 shell 寫成,使⽤ Java 重寫⼯作量巨⼤。

問題原因:傳統⾃動化測試思路中,我們的測試邏輯是在以硬編碼的形式組織到代碼⾥的,當遇到⽤例遷移或結果整合時,會產⽣⼤量的邏輯重寫。

解決思路:除了硬編碼的腳本編寫⽅式外,還要能動態地在腳本 Runtime 時⽣成⽤例。

實施⽅案:JUnit5 提供了動態測試⽅案,讓測試⼈員可以在腳本 Runtime 時動態的批量⽣成⽤例。

官⽅給出的 DynamicTest ⽰例

image

案例實現

  • Maven 依賴:
       <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>2.9.9</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>
  • 配置文件:shell_test_result.yaml
  resultList:
    - caseName : 'case_1_1'
      result : true
    - caseName : 'case_1_2'
      result : false
    - caseName : 'case_2_1'
      result : true
    - caseName : 'case_2_2'
      result : false
    - caseName: 'case_1_1'
      result: true
    - caseName: 'case_1_2'
      result: false
    - caseName: 'case_2_1'
      result: true
    - caseName: 'case_2_2'
      result: false
    - caseName: 'case_1_1'
      result: true
    - caseName: 'case_1_2'
      result: false
    - caseName: 'case_2_1'
      result: true
    - caseName: 'case_2_2'
      result: false
  • 實體類:ShellResult.java
import lombok.Data;

@Data  // 實現了getter和setter功能
public class ShellResult {
    private String caseName;
    private boolean result;
}
  • 實體類:ResultList.java
import lombok.Data;

import java.util.List;

@Data
public class ResultList {
    private List<ShellResult> resultList;
}
  • 測試類:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import entity.ResultList;
import entity.ShellResult;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;


public class Demo {

    // 批量讀取測試文件數據,動態生成並執行測試用例
    @TestFactory
    Collection<DynamicTest> runShellResult() throws IOException {
        List<DynamicTest> dynamicTestList = new ArrayList<>();
        ObjectMapper objectMApper = new ObjectMapper(new YAMLFactory());
        // 反序列化yaml數據到對象列表中
        ResultList resultList = objectMApper.readValue(new File("src/main/resources/shell_test_result.yaml"), ResultList.class);
        System.out.println("Done!");
        // 動態遍歷生成測試方法
        for(ShellResult shellResult: resultList.getResultList()){
            // 動態生成測試方法代碼
            DynamicTest dynamicTest=dynamicTest(
                    shellResult.getCaseName(),
                    () -> {assertTrue(shellResult.isResult());}
            );
            // 收集操作
            dynamicTestList.add(dynamicTest);
        }
        return dynamicTestList;
    }

}
  • 執行結果:

image


並發測試

默認情況下,JUnit Jupiter 測試是在單個線程中按順序運行的。自 5.3 版起,作為可選功能,可以並發執行測試。

並發測試前提條件:測試用例之間沒有依賴關系,容錯性好。

Junit5 的並發測試是在 junit-platform.properties 文件中進行配置的,只要在 src/main/resources/ 目錄下新建該文件,那么 Junit5 在執行測試方法時就會自動讀取配置文件中的配置。

示例

src/main/resources/junit-platform.properties 文件內容:

# 是否開啟並發執行
junit.jupiter.execution.parallel.enabled = true
# 是否支持方法級別多線程,參數為:same_thread/concurrent
junit.jupiter.execution.parallel.mode.default = concurrent
# 是否支持類級別多線程,參數為:same_thread/concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
# 指定線程池大小
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=2

測試程序:

import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.parallel.Execution;

import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;

public class ConcurrentTest {

    @RepeatedTest(10)
    @Execution(CONCURRENT)
    void testConcurrent() {
        System.out.println("當前線程id:" + Thread.currentThread().getId());
    }

}

配置文件詳解

Junit5 官方 User guide 對每個配置項進行了簡單的解釋:

junit.jupiter.execution.parallel.enabled=true

要啟用並發執行,請將以上配置參數設置為 true。

# 是否支持方法級別多線程same_thread/concurrent
junit.jupiter.execution.parallel.mode.default=same_thread
# 是否支持類級別多線程same_thread/concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

以上兩個配置項是用來控制測試腳本是否在方法維度和類維度進行並發執行。它們具有相同的兩個配置選項,官方 User guide 解釋如下:

  • SAME_THREAD:強制與前置方法使用的同一線程執行。例如,當在測試方法上使用時,該測試方法將在 @BeforeAll 或 @AfterAll 方法的線程中執行。
  • CONCURRENT:並發執行,除非資源鎖強制在同一線程中執行。

說白了就是 SAME_THREAD 意味着單線程,而 CONCURRENT 意味着多線程。

官方 User guide 中有一個圖來解讀這兩個配置項組合能達到什么樣的效果:

image

線程池配置的三種方式

剩下這兩個配置是用來控制線程池屬性的:

# the maximum pool size can be configured using a ParallelExecutionConfigurationStrategy
junit.jupiter.execution.parallel.config.strategy = fix
junit.jupiter.execution.parallel.config.fixed.parallelism = 2
  1. dynamic:根據可用邏輯處理器數量乘以 junit.jupiter.execution.parallel.config.dynamic.factor 的配置參數(默認為 1)計算並行的線程池數量。也就是說配置一個固定的倍數后,框架會根據運行機器的算力自動配置線程。

  2. fixed:使用 junit.jupiter.execution.parallel.config.fixed.parallelism 的配置參數作為並行的線程池數量。

  3. custom:允許通過實現接口 ParallelExecutionConfigurationStrategy 來配置並行的線程池數量,junit.jupiter.execution.parallel.config.custom.class 屬性用來配置實現接口的實現類。


測試套件

使用 JUnit5 測試套件,可以將測試擴展到多個測試類和不同的軟件包。

JUnit5 提供了兩個注解:@SelectPackages 和 @SelectClasses 來創建測試套件。而要執行該套件,需要配合使用 @RunWith(JUnitPlatform.class)。

注解 作用
@RunWith(JUnitPlatform.class) 測試套件(從 JUnit4 遷移過來的)
@SelectPackage 創建測試套件
@SelectClasses 創建測試套件
@IncludePackage 過濾需要執行的測試包
@ExcludePackages 過濾不需要執行的測試包
@IncludeClassNamePatterns 過濾需要執行的測試類
@ExcludeClassNamePatterns 過濾不需要執行的測試類
@IncludeTags 過濾需要執行的測試方法
@ExcludeTags 過濾不需要執行的測試方法

添加 Maven 依賴:

<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-runner</artifactId>
    <version>1.6.1</version>
    <scope>test</scope>
</dependency>

示例:@RunWith、@SelectPackages

import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
public class SuiteTest {
}

image


示例:@SelectPackages、@SelectClasses

@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1"})
@SelectClasses({BTest.class, CTest.class})
public class SuiteTest {
    // 執行 @SelectPackages 和 @SelectClasses 並集后的測試方法
}

image


示例:@IncludePackages、@ExcludePackages

import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.ExcludePackages;
import org.junit.platform.suite.api.IncludePackages;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
@IncludePackages({"com.demo1", "com.demo2"})
@ExcludePackages("com.demo2")
public class SuiteTest {
}

image


示例:@IncludeClassNamePatterns、@ExcludeClassNamePatterns

import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.*;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
@IncludeClassNamePatterns("com.*Test")
@ExcludeClassNamePatterns({
        "com.innder.*",
        "com.*.CTest"
})
public class SuiteTest {
}

image


示例:@IncludeTags、@Tag

import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
@IncludeTags("TagDemo")
public class SuiteTest {
}

image


maven-surefire-plugin

什么是 maven-surefire-plugin ?

如果你執行過 mvn test 或者執行其他 maven 命令時跑了測試用例,你就已經用過 maven-surefire-plugin 了。maven-surefire-plugin 是 maven 里執行測試用例的插件,不顯示配置就會用默認配置。這個插件的 surefire:test 命令會默認綁定 maven 執行的 test 階段。

maven的生命周期有哪些階段?

[validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy]

maven-surefire-plugin 的使用

如果說 maven 已經有了 maven-surefire-plugin 的默認配置,我們還有必要了解 maven-surefire-plugin 的配置么?答案是肯定的。雖說 maven-surefire-plugin 有默認配置,但是當需要修改一些測試執行的策略時,就有必要我們去重新配置這個插件了。

插件自動匹配

最簡單的配置方式就不配置或者是只聲明插件。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19</version>
</plugin>

這個時候 maven-surefire-plugin 會按照如下邏輯去尋找 JUnit 的版本並執行測試用例。

if the JUnit version in the project >= 4.7 and the parallel attribute has ANY value
    use junit47 provider
if JUnit >= 4.0 is present
    use junit4 provider
else
    use junit3.8.1

插件手動匹配

示例:

    <build>
        <plugins>
            <!-- 該插件能夠在運行后自動在target目錄生成allure測試結果目錄 -->
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
                <configuration>
                    <includes>
                        <!-- 默認測試文件的命名規則:
                            "**/Test*.java"
                            "**/*Test.java"
                            "**/*Tests.java"
                            "**/*TestCase.java"
                            如果現有測試文件不符合以上命名,可以在 pom.xml 添加自定義規則
                        -->
                        <include>**/**.java</include>
                    </includes>
                </configuration>
            </plugin>
        </plugins>
    </build>

更多用法

maven-surefire-plugin 更多用法


免責聲明!

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



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