JUnit5學習之八:綜合進階(終篇)

flynike發表於2021-09-09

歡迎訪問我的GitHub

github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概覽

  • 本文是《JUnit5學習》系列的終篇,將JUnit5提供的一些高階特性以實戰的形式展現出來;
  • JUnit5的特性非常多,《JUnit5學習》系列也只是將常用部分寫出來,未能覆蓋全部;
  • 本文由以下章節組成:
  1. 版本設定
  2. 測試方法展現名稱生成器
  3. 重複測試
  4. 巢狀
  5. 動態測試(Dynamic Tests)
  6. 多執行緒併發執行測試方法

原始碼下載

  1. 如果您不想編碼,可以在GitHub下載所有原始碼,地址和連結資訊如下表所示:
名稱 連結 備註
專案主頁 github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh) :zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議
  1. 這個git專案中有多個資料夾,本章的應用在junitpractice資料夾下,如下圖紅框所示:

圖片描述

  1. junitpractice是父子結構的工程,本篇的程式碼在advanced子工程中,如下圖:

圖片描述

版本設定

  • 《JUnit5學習》系列的程式碼都在用SpringBoot:2.3.4.RELEASE框架,間接依賴的JUnit版本是5.6.2
  • 本文有兩個特性要求JUnit版本達到5.7或者更高,它們是測試方法展現名稱生成器動態生成測試方法
  • 對於使用SpringBoot:2.3.4.RELEASE框架的工程,如果要指定JUnit版本,需要做以下三步操作:
  1. dependencyManagement節點新增junit-bom,並指定版本號:
org.junitjunit-bom5.7.0pomimport
  1. 排除spring-boot-starter-test和junit-jupiter的間接依賴關係:
org.springframework.bootspring-boot-starter-testtestorg.junit.jupiterjunit-jupiter
  1. 新增junit-jupiter依賴,此時會使用dependencyManagement中指定的版本號:
org.junit.jupiterjunit-jupitertest
  1. 如下圖,重新整理可見已經用上了5.7.0版本:

圖片描述

  • 版本問題解決了,接下來正式進入進階實戰;

測試方法展現名稱生成器(Display Name Generators)

  1. Display Name Generators翻譯成測試方法展現名稱生成器,可能重新整理了讀者們對本文作者英文水平的認知,請您多包含…
  2. 先回顧一下如何指定測試方法的展現名稱,如果測試方法使用了@DisplayName,在展示單元測試執行結果時,就會顯示@DisplayName指定的字串,如下圖所示:

圖片描述
3. 除了用@DisplayName指定展示名稱,JUnit5還提供了一種自動生成展示名稱的功能:@DisplayNameGeneration,來看看它是如何生成展示名稱的;
4. 演示程式碼如下所示,當@DisplayNameGeneration的value設定為ReplaceUnderscores時,會把方法名的所有下劃線替換為空格:

package com.bolingcavalry.advanced.service.impl;

import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class ReplaceUnderscoresTest {

    @Test
    void if_it_is_zero() {
    }
}
  1. 執行結果如下圖,方法if_it_is_zero展示出的名字為if it is zero

圖片描述
6. 在上述替換方式的基礎上,JUnit5還提供了另一種生成展示名稱的方法:測試類名+連線符+測試方法名,並且類名和方法名的下劃線都會被替換成空格,演示程式碼如下,使用了註解@IndicativeSentencesGeneration,其separator屬性就是類名和方法名之間的連線符:

package com.bolingcavalry.advanced.service.impl;

import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@IndicativeSentencesGeneration(separator = ",測試方法:", generator = DisplayNameGenerator.ReplaceUnderscores.class)
public class IndicativeSentences_Test {

    @Test
    void if_it_is_one_of_the_following_years() {
    }
}
  1. 執行結果如下:

圖片描述

重複測試(Repeated Tests)

  1. 重複測試就是指定某個測試方法反覆執行多次,演示程式碼如下,可見@Test已被@RepeatedTest(5)取代,數字5表示重複執行5次:
    @Order(1)
    @DisplayName("重複測試")
    @RepeatedTest(5)
    void repeatTest(TestInfo testInfo) {
        log.info("測試方法 [{}]", testInfo.getTestMethod().get().getName());
    }
  1. 執行結果如下圖:

圖片描述
3. 在測試方法執行時,如果想了解當前是第幾次執行,以及總共有多少次,只要給測試方法增加RepetitionInfo型別的入參即可,演示程式碼如下,可見RepetitionInfo提供的API可以得到總數和當前次數:

    @Order(2)
    @DisplayName("重複測試,從入參獲取執行情況")
    @RepeatedTest(5)
    void repeatWithParamTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
                testInfo.getTestMethod().get().getName(),
                repetitionInfo.getCurrentRepetition(),
                repetitionInfo.getTotalRepetitions());
    }
  1. 上述程式碼執行結果如下:

圖片描述
5. 在上圖的左下角可見,重複執行的結果被展示為"repetition X of X"這樣的內容,其實這部分資訊是可以定製的,就是RepeatedTest註解的name屬性,演示程式碼如下,可見currentRepetitiontotalRepetitions是佔位符,在真正展示的時候會被分別替換成當前值和總次數:

    @Order(3)
    @DisplayName("重複測試,使用定製名稱")
    @RepeatedTest(value = 5, name="完成度:{currentRepetition}/{totalRepetitions}")
    void repeatWithCustomDisplayNameTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
                testInfo.getTestMethod().get().getName(),
                repetitionInfo.getCurrentRepetition(),
                repetitionInfo.getTotalRepetitions());
    }
  1. 上述程式碼執行結果如下:

圖片描述

巢狀測試(Nested Tests)

  1. 如果一個測試類中有很多測試方法(如增刪改查,每種操作都有多個測試方法),那麼不論是管理還是結果展現都會顯得比較複雜,此時巢狀測試(Nested Tests)就派上用場了;
  2. 巢狀測試(Nested Tests)功能就是在測試類中建立一些內部類,以增刪改查為例,將所有測試查詢的方法放入一個內部類,將所有測試刪除的方法放入另一個內部類,再給每個內部類增加@Nested註解,這樣就會以內部類為單位執行測試和展現結果,如下圖所示:

圖片描述
3. 巢狀測試的演示程式碼如下:

package com.bolingcavalry.advanced.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
@DisplayName("巢狀測試演示")
public class NestedTest {

    @Nested
    @DisplayName("查詢服務相關的測試")
    class FindService {
        @Test
        void findByIdTest() {}
        @Test
        void findByNameTest() {}
    }

    @Nested
    @DisplayName("刪除服務相關的測試")
    class DeleteService {
        @Test
        void deleteByIdTest() {}
        @Test
        void deleteByNameTest() {}
    }
}
  1. 上述程式碼執行結果如下,可見從程式碼管理再到執行和結果展示,都被分組管理了:

圖片描述

動態測試(Dynamic Tests)

  1. 之前我們們寫的測試方法,主要是用@Test修飾,這些方法的特點就是在編譯階段就已經明確了,在執行階段也已經固定;
  2. JUnit5推出了另一種型別的測試方法:動態測試(Dynamic Tests),首先,測試方法是可以在執行期間被生產出來的,生產它們的地方,就是被@TestFactory修飾的方法,等到測試方法被生產出來後再像傳統的測試方法那樣被執行和結果展示;
  3. 下面是演示程式碼,testFactoryTest方法被@TestFactory修飾,返回值是Iterable型別,裡面是多個DynamicTest例項,每個DynamicTest例項代表一個測試方法,因此,整個DynamicDemoTest類中有多少個測試方法,在編譯階段是不能確定的,只有在執行階段執行了testFactoryTest方法後,才能根據返回值確定下來:
package com.bolingcavalry.advanced.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

@SpringBootTest
@Slf4j
class DynamicDemoTest {

    @TestFactory
    Iterable testFactoryTest() {

        DynamicTest firstTest = dynamicTest(
            "一號動態測試用例",
            () -> {
                log.info("一號用例,這裡編寫單元測試邏輯程式碼");
            }
        );

        DynamicTest secondTest = dynamicTest(
                "二號動態測試用例",
                () -> {
                    log.info("二號用例,這裡編寫單元測試邏輯程式碼");
                }
        );

        return Arrays.asList(firstTest, secondTest);
    }
}
  1. 上述程式碼的執行結果如下,可見每個DynamicTest例項就相當於以前的一個@Test修飾的方法,會被執行和統計:

圖片描述

多執行緒併發執行(Parallel Execution)的介紹

  • 《JUnit5學習》系列的最後,我們們來看一個既容易理解又實用的特性:多執行緒併發執行(Parallel Execution)
  • JUnit5中的併發執行測試可以分為以下三種場景:
  1. 多個測試類,它們各自的測試方法同時執行;
  2. 一個測試類,裡面的多個測試方法同時執行;
  3. 一個測試類,裡面的一個測試方法,在重複測試(Repeated Tests)或者引數化測試(Parameterized Tests)的時候,這個測試方法被多個執行緒同時執行;

多執行緒併發執行(Parallel Execution)實戰

  1. 前面介紹了多執行緒併發執行有三種場景,文章篇幅所限就不逐個編碼實戰了,就選擇第三種場景來實踐吧,即:一個測試類裡面的一個測試方法,在重複測試時多執行緒併發執行,至於其他兩種場景如何設定,接下來的文中也會講清楚,您自行實踐即可;
  2. 首先是建立JUnit5的配置檔案,如下圖,在test資料夾上點選滑鼠右鍵,在彈出的選單選擇"New"->“Directory”:

圖片描述

  1. 彈出的視窗如下圖,雙擊紅框位置的"resources",即可新建resources目錄:

圖片描述

  1. 在新增的resources目錄中新建檔案junit-platform.properties,內容如下,每個配置項都有詳細的說明:
# 並行開關true/false
junit.jupiter.execution.parallel.enabled=true
# 方法級多執行緒開關 same_thread/concurrent
junit.jupiter.execution.parallel.mode.default = same_thread
# 類級多執行緒開關 same_thread/concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread

# 併發策略有以下三種可選:
# fixed:固定執行緒數,此時還要透過junit.jupiter.execution.parallel.config.fixed.parallelism指定執行緒數
# dynamic:表示根據處理器和核數計算執行緒數
# custom:自定義併發策略,透過這個配置來指定:junit.jupiter.execution.parallel.config.custom.class
junit.jupiter.execution.parallel.config.strategy = fixed

# 併發執行緒數,該配置項只有當併發策略為fixed的時候才有用
junit.jupiter.execution.parallel.config.fixed.parallelism = 5
  1. 由於實踐的是同一個類同一個方法多次執行的併發,因此上述配置中,類級多執行緒開關和方法級多執行緒開關都選擇了"同一個執行緒",也就是說不需要併發執行多個類或者多個方法,請您根據自己的需求自行調整;
  2. 關於併發策略,這裡選擇的是動態調整,我這裡是i5-8400處理器,擁有六核心六執行緒,稍後我們們看看執行效果與這個硬體配置是否有關係;
  3. 接下來編寫測試程式碼,先寫一個單執行緒執行的,可見@Execution的值為SAME_THREAD,限制了重複測試時在同一個執行緒內順序執行:
package com.bolingcavalry.advanced.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ParallelExecutionTest {

    @Order(1)
    @Execution(ExecutionMode.SAME_THREAD)
    @DisplayName("單執行緒執行10次")
    @RepeatedTest(value = 10, name="完成度:{currentRepetition}/{totalRepetitions}")
    void sameThreadTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
                testInfo.getTestMethod().get().getName(),
                repetitionInfo.getCurrentRepetition(),
                repetitionInfo.getTotalRepetitions());
    }
}
  1. 執行結果如下,可見確實是單執行緒:

圖片描述

  1. 重複測試時併發執行的程式碼如下,@Execution的值為CONCURRENT
    @Order(2)
    @Execution(ExecutionMode.CONCURRENT)
    @DisplayName("多執行緒執行10次")
    @RepeatedTest(value = 10, name="完成度:{currentRepetition}/{totalRepetitions}")
    void concurrentTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
                testInfo.getTestMethod().get().getName(),
                repetitionInfo.getCurrentRepetition(),
                repetitionInfo.getTotalRepetitions());
    }
  1. 執行結果如下,從紅框1可見順序已經亂了,從紅框2可見十次測試方法是在五個執行緒中執行的:

圖片描述
11. 最後是引數化測試的演示,也可以設定為多執行緒並行執行:

    @Order(3)
    @Execution(ExecutionMode.CONCURRENT)
    @DisplayName("多個int型入參")
    @ParameterizedTest
    @ValueSource(ints = { 1,2,3,4,5,6,7,8,9,0 })
    void intsTest(int candidate) {
        log.info("ints [{}]", candidate);
    }
  1. 執行結果如下圖,可見也是5個執行緒並行執行的:

圖片描述

結束語

至此,《JUnit5學習》系列已經全部完成,感謝您的耐心閱讀,希望這個原創系列能夠帶給您一些有用的資訊,為您的單元測試提供一些參考,如果發現文章有錯誤,期待您能指點一二;

我是欣宸,期待與您一同暢遊Java世界…

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2618/viewspace-2827038/,如需轉載,請註明出處,否則將追究法律責任。

相關文章