一、JUnit 簡介:什么是 JUnit?為什么要用它?
1.1 核心定義
JUnit 是一個開源的、基于 Java 語言的單元測試框架,最初由 Erich Gamma (GoF 設計模式作者之一) 和 Kent Beck (極限編程創始人) 在 1997 年共同開發。作為 xUnit 測試框架家族中最重要的成員,JUnit 目前最新穩定版本為 JUnit 5(代號 Jupiter),于 2017 年發布。
JUnit 的核心作用是幫助開發者:
- 編寫結構化、可維護的單元測試代碼
- 自動化執行測試用例
- 生成詳細的測試報告
- 通過斷言機制驗證代碼行為是否符合預期
典型測試場景示例:
@Test
void testAddition() {Calculator calc = new Calculator();assertEquals(5, calc.add(2, 3)); // 驗證2+3是否等于5
}
1.2 為什么選擇 JUnit?
簡單易用:
- 采用注解驅動(如 @Test、@BeforeEach)
- 提供豐富的斷言方法(assertEquals、assertTrue 等)
- 基本測試用例僅需5行代碼即可完成
IDE 無縫集成:
- IntelliJ IDEA:內置支持,可一鍵運行測試并顯示彩色結果
- Eclipse:自帶 JUnit 視圖,支持測試覆蓋率分析
- VS Code:通過插件提供完整測試支持
生態完善:
- 構建工具:
- Maven:通過 surefire 插件執行測試
- Gradle:內置 test 任務支持
- 框架整合:
- Spring Boot 提供 @SpringBootTest 注解
- Mockito 等模擬框架完美兼容
- 構建工具:
進階功能:
- 參數化測試(@ParameterizedTest):
@ParameterizedTest @ValueSource(ints = {1, 3, 5}) void testOddNumbers(int number) {assertTrue(number % 2 != 0); }
- 測試套件(@Suite)
- 動態測試(@TestFactory)
- 條件測試(@EnabledOnOs)
- 參數化測試(@ParameterizedTest):
二、JUnit 5 環境搭建:從依賴引入到第一個測試用例
1. JUnit 5 架構組成
JUnit 5 采用了模塊化設計,由三個核心模塊組成:
JUnit Jupiter
- 包含 JUnit 5 的核心 API,如測試注解(
@Test
,@BeforeEach
等)和斷言方法(assertEquals()
,assertTrue()
等) - 引入了新的編程模型和擴展模型
- 示例:
@ParameterizedTest
支持參數化測試,能更靈活地編寫測試用例
- 包含 JUnit 5 的核心 API,如測試注解(
JUnit Vintage
- 提供向后兼容支持,允許運行 JUnit 3 和 JUnit 4 編寫的測試用例
- 在遷移項目中尤其有用,可以逐步將舊測試遷移到 JUnit 5
- 需要額外依賴
junit-vintage-engine
JUnit Platform
- 提供統一的測試運行平臺,作為測試執行的基礎
- 支持在 IDE(如 IntelliJ IDEA, Eclipse)、構建工具(Maven, Gradle)中執行測試
- 允許通過命令行啟動測試
- 提供測試發現和執行的API
2. 項目配置
2.1 Maven 依賴配置
完整的 Maven 配置示例如下:
<dependencies><!-- JUnit 5核心API --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.9.2</version><scope>test</scope></dependency><!-- JUnit 5測試引擎(運行時必需) --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>5.9.2</version><scope>test</scope></dependency><!-- 可選:參數化測試支持 --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-params</artifactId><version>5.9.2</version><scope>test</scope></dependency>
</dependencies><build><plugins><!-- 配置Maven Surefire插件以支持JUnit 5 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.1.2</version><configuration><includes><include>**/*Test.java</include></includes></configuration></plugin></plugins>
</build>
3. 編寫測試用例
3.1 業務類實現
/*** 計算器工具類* 提供基本的加減運算功能*/
public class Calculator {/*** 加法運算* @param a 第一個操作數* @param b 第二個操作數* @return 兩數之和*/public int add(int a, int b) {return a + b;}/*** 減法運算* @param a 被減數* @param b 減數* @return 兩數之差*/public int subtract(int a, int b) {return a - b;}/*** 除法運算* @param dividend 被除數* @param divisor 除數* @return 除法結果* @throws ArithmeticException 當除數為0時拋出*/public double divide(int dividend, int divisor) {if (divisor == 0) {throw new ArithmeticException("除數不能為0");}return (double) dividend / divisor;}
}
3.2 測試類實現
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;import static org.junit.jupiter.api.Assertions.*;/*** Calculator類的單元測試*/
@DisplayName("計算器功能測試")
class CalculatorTest {private Calculator calculator;/*** 在每個測試方法執行前初始化Calculator實例*/@BeforeEachvoid setUp() {calculator = new Calculator();}@Test@DisplayName("加法功能測試 - 正常情況")void testAdd() {assertEquals(5, calculator.add(2, 3), "2+3應該等于5");assertEquals(0, calculator.add(-1, 1), "-1+1應該等于0");}@Test@DisplayName("減法功能測試")void testSubtract() {assertEquals(1, calculator.subtract(3, 2), "3-2應該等于1");assertEquals(-5, calculator.subtract(0, 5), "0-5應該等于-5");}@ParameterizedTest@CsvSource({"6, 2, 3","10, 5, 2","-4, -8, 2"})@DisplayName("除法功能參數化測試")void testDivide(int dividend, int divisor, double expected) {assertEquals(expected, calculator.divide(dividend, divisor), () -> dividend + "除以" + divisor + "應該等于" + expected);}@Test@DisplayName("除法異常測試 - 除數為0")void testDivideByZero() {ArithmeticException exception = assertThrows(ArithmeticException.class,() -> calculator.divide(1, 0),"除數為0時應拋出ArithmeticException");assertEquals("除數不能為0", exception.getMessage());}
}
4. 測試執行與報告
4.1 執行方式
IDE 執行:
- IntelliJ IDEA:右鍵測試類 → "Run 'CalculatorTest'"
- Eclipse:右鍵測試類 → "Run As" → "JUnit Test"
- 可以執行單個測試方法、整個測試類或整個測試包
Maven 命令行執行:
mvn test # 執行所有測試 mvn -Dtest=CalculatorTest test # 執行特定測試類 mvn -Dtest=CalculatorTest#testAdd test # 執行特定測試方法
Gradle 執行:
gradle test # 執行所有測試 gradle test --tests CalculatorTest # 執行特定測試類
4.2 測試結果分析
測試通過:
- IDE 中顯示綠色標記
- 控制臺輸出類似:
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS
測試失敗:
- IDE 中顯示紅色標記
- 控制臺輸出詳細錯誤信息,包括:
- 失敗的方法名
- 預期值和實際值差異
- 失敗位置(代碼行號)
- 自定義錯誤信息(如果有)
[ERROR] testAdd(CalculatorTest) Time elapsed: 0.012 s <<< FAILURE! org.opentest4j.AssertionFailedError: 2+3應該等于5 ==> Expected :5 Actual :6
4.3 高級功能
生命周期鉤子:
@BeforeAll // 在測試類執行前運行一次 static void initAll() { /* 初始化代碼 */ }@AfterEach // 在每個測試方法執行后運行 void tearDown() { /* 清理代碼 */ }@AfterAll // 在測試類執行后運行一次 static void tearDownAll() { /* 最終清理 */ }
斷言增強:
// 多條件斷言 assertAll("多條件驗證",() -> assertEquals(5, result),() -> assertTrue(result > 0) );// 超時斷言 assertTimeout(Duration.ofMillis(100), () -> {// 應在100ms內完成的操作 });
標簽和過濾:
@Tag("fast") @Test void fastTest() { /* ... */ }@Tag("slow") @Test void slowTest() { /* ... */ }
三、JUnit 5 核心注解:掌握測試流程控制
JUnit 5 提供了一系列注解用于標記測試方法和控制測試生命周期。這些注解可以幫助開發者更有效地組織和執行測試用例。
核心生命周期注解
注解 | 作用 | 重要說明 |
---|---|---|
@Test | 標記一個方法為測試方法 | 方法必須為void 返回類型且無參數 |
@BeforeEach | 每個測試方法執行前運行 | 常用于初始化測試對象(如創建待測試類實例) |
@AfterEach | 每個測試方法執行后運行 | 常用于釋放資源(如關閉文件句柄、數據庫連接) |
@BeforeAll | 所有測試方法執行前運行一次 | 必須是靜態方法,常用于加載全局配置(如數據庫連接池初始化) |
@AfterAll | 所有測試方法執行后運行一次 | 必須是靜態方法,常用于清理全局資源(如關閉數據庫連接池) |
測試控制注解
注解 | 作用 | 使用場景示例 |
---|---|---|
@Disabled | 標記測試方法/類為"禁用",不參與測試執行 | 方法未完成時臨時跳過測試;某些環境不支持的測試用例 |
@DisplayName | 為測試方法/類設置自定義顯示名稱 | 使用中文描述測試目的(如@DisplayName("用戶登錄失敗場景測試") ) |
@Timeout | 設置測試方法超時時間 | 性能測試(如@Timeout(500) 表示500毫秒內未完成則測試失敗) |
進階用法示例
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;@DisplayName("計算器功能測試套件")
class AdvancedCalculatorTest {// 共享測試資源private static DatabaseConnection dbConnection;private Calculator calculator;@BeforeAllstatic void initAll() throws Exception {dbConnection = new DatabaseConnection("jdbc:mysql://localhost/test");dbConnection.connect();System.out.println("數據庫連接建立完成");}@BeforeEachvoid init() {calculator = new ScientificCalculator(dbConnection);System.out.println("初始化科學計算器實例");}@Test@DisplayName("復雜公式計算:(2^3 + √16) × 5")@Timeout(1000)void testComplexCalculation() {double result = calculator.calculate("(pow(2,3)+sqrt(16))*5");assertEquals(60.0, result, 0.001);}@Test@Disabled("等待數據庫函數修復")void testDatabaseFunction() {// 測試使用數據庫函數的計算}@AfterEachvoid cleanup() {calculator.reset();System.out.println("清理計算器狀態");}@AfterAllstatic void tearDownAll() {dbConnection.close();System.out.println("數據庫連接已關閉");}
}
測試執行順序說明
- 首先執行
@BeforeAll
標記的方法(僅一次) - 對每個測試方法:
- 執行
@BeforeEach
方法 - 執行
@Test
方法 - 執行
@AfterEach
方法
- 執行
- 最后執行
@AfterAll
標記的方法(僅一次)
典型應用場景
數據庫測試:
@BeforeAll
建立連接池@BeforeEach
開始事務@AfterEach
回滾事務@AfterAll
關閉連接池
性能測試:
@Test @Timeout(value = 100, unit = TimeUnit.MILLISECONDS) void shouldRespondIn100Milliseconds() {// 測試響應時間 }
條件測試:
@Test @EnabledOnOs(OS.LINUX) void linuxOnlyTest() {// 僅在Linux系統執行的測試 }
四、JUnit 5 斷言方法:驗證測試結果的核心
斷言是單元測試的核心組成部分,用于判斷 "實際結果" 是否與 "預期結果" 一致。JUnit 5 的 org.junit.jupiter.api.Assertions
類提供了豐富的斷言方法,這些方法可以幫助開發者編寫清晰、可讀性強的測試代碼。
4.1 基本斷言(數值、字符串、布爾值)
基本斷言是最常用的斷言類型,用于驗證基本數據類型、對象、布爾條件等。
詳細方法說明
方法 | 功能描述 | 適用場景 |
---|---|---|
assertEquals(expected, actual) | 驗證兩個值相等 | 比較計算結果與預期值、對象相等性判斷 |
assertNotEquals(expected, actual) | 驗證兩個值不相等 | 確保兩個對象不相同 |
assertTrue(condition) | 驗證條件為 true | 布爾表達式驗證 |
assertFalse(condition) | 驗證條件為 false | 布爾表達式驗證 |
assertNull(object) | 驗證對象為 null | 空值檢查 |
assertNotNull(object) | 驗證對象不為 null | 非空檢查 |
擴展示例
@Test
void testExtendedBasicAssertions() {// 精度控制的數值比較assertEquals(0.333, 1.0/3.0, 0.001, "除法精度驗證失敗");// 字符串比較String expectedStr = "Hello";String actualStr = "HELLO".toLowerCase();assertEquals(expectedStr, actualStr, "字符串轉換驗證失敗");// 對象比較(需實現equals方法)Person p1 = new Person("John", 30);Person p2 = new Person("John", 30);assertEquals(p1, p2, "對象相等性驗證失敗");// 鏈式斷言String message = "Hello World";assertAll("message屬性驗證",() -> assertNotNull(message),() -> assertTrue(message.startsWith("Hello")),() -> assertTrue(message.endsWith("World")));
}
4.2 數組與集合斷言
數組和集合斷言專門用于驗證數組或集合類型的數據結構。
詳細方法說明
方法 | 功能描述 | 適用場景 |
---|---|---|
assertArrayEquals(expected, actual) | 驗證兩個數組內容相等 | 基本類型數組、對象數組比較 |
assertIterableEquals(expected, actual) | 驗證兩個集合內容相等 | List、Set等集合類型比較 |
擴展示例
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.HashSet;@Test
void testExtendedArrayAndIterable() {// 多維數組比較int[][] expectedMatrix = {{1,2}, {3,4}};int[][] actualMatrix = {{1,2}, {3,4}};assertArrayEquals(expectedMatrix, actualMatrix);// 集合順序不敏感的比較Set<String> expectedSet = new HashSet<>(Arrays.asList("a", "b", "c"));Set<String> actualSet = new HashSet<>(Arrays.asList("c", "b", "a"));assertEquals(expectedSet, actualSet);// 使用自定義比較器List<String> names = Arrays.asList("John", "Alice", "Bob");assertTrue(names.containsAll(Arrays.asList("Alice", "Bob")), "集合應包含指定元素");// 集合大小驗證List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);assertEquals(5, numbers.size(), "集合大小不正確");
}
4.3 異常斷言
異常斷言用于驗證代碼是否按預期拋出特定異常。
詳細方法說明
方法 | 功能描述 | 適用場景 |
---|---|---|
assertThrows(異常類型.class, 可執行代碼) | 驗證是否拋出指定異常 | 邊界條件、非法輸入驗證 |
擴展示例
// 業務方法:文件讀取
public String readFile(String path) throws IOException {if (path == null) {throw new IllegalArgumentException("路徑不能為null");}if (!new File(path).exists()) {throw new FileNotFoundException("文件不存在");}return Files.readString(Paths.get(path));
}// 測試異常拋出
@Test
void testFileOperations() {FileProcessor processor = new FileProcessor();// 驗證空路徑異常IllegalArgumentException nullEx = assertThrows(IllegalArgumentException.class,() -> processor.readFile(null));assertEquals("路徑不能為null", nullEx.getMessage());// 驗證文件不存在異常FileNotFoundException notFoundEx = assertThrows(FileNotFoundException.class,() -> processor.readFile("nonexistent.txt"));assertTrue(notFoundEx.getMessage().contains("不存在"));// 驗證無異常情況assertDoesNotThrow(() -> processor.readFile("existing.txt"),"正常文件讀取不應拋出異常");
}
4.4 超時斷言
超時斷言用于驗證方法執行時間是否符合預期。
詳細方法說明
方法 | 功能描述 | 適用場景 |
---|---|---|
assertTimeout(時間, 可執行代碼) | 驗證代碼在指定時間內完成 | 性能測試、算法效率驗證 |
assertTimeoutPreemptively(時間, 可執行代碼) | 超時立即終止測試 | 嚴格時間限制的場景 |
擴展示例
@Test
void testExtendedTimeout() {// 簡單超時驗證assertTimeout(Duration.ofMillis(100), () -> {// 模擬耗時操作Thread.sleep(50);});// 帶返回值的超時驗證String result = assertTimeout(Duration.ofSeconds(1), () -> {Thread.sleep(500);return "Done";});assertEquals("Done", result);// 嚴格超時(超時立即終止)assertTimeoutPreemptively(Duration.ofMillis(100), () -> {// 如果耗時超過100ms會立即終止Thread.sleep(50);});// 性能基準測試long executionTime = assertTimeout(Duration.ofSeconds(2), () -> {long start = System.currentTimeMillis();// 執行待測方法performComplexCalculation();return System.currentTimeMillis() - start;});assertTrue(executionTime < 1000, "方法執行時間過長");
}
五、JUnit 5 進階功能:提升測試效率
5.1 參數化測試(重復執行不同參數的測試)
參數化測試是JUnit 5中強大的功能之一,它允許開發者通過提供多組輸入參數來重復執行同一個測試邏輯。相比傳統測試方法只能固定使用一組參數進行測試,參數化測試顯著提高了測試覆蓋率和代碼復用性。
實現原理與技術要點
參數化測試需要兩個核心注解配合使用:
@ParameterizedTest
:標記方法為參數化測試方法- 參數源注解:提供具體參數值,如
@ValueSource
、@CsvSource
等
JUnit 5內置了多種參數源類型:
- 簡單值:
@ValueSource
(適用于單參數) - CSV格式:
@CsvSource
(適用于多參數組合) - 方法提供:
@MethodSource
(通過方法返回參數流) - 枚舉值:
@EnumSource
- 文件內容:
@CsvFileSource
(從CSV文件讀取)
詳細示例解析
示例1:單參數測試(@ValueSource)
測試計算器類的isPositive
方法,判斷數字是否為正數:
// 業務方法實現
public class Calculator {public boolean isPositive(int num) {return num > 0;}
}// 測試類
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;class CalculatorTest {private final Calculator calculator = new Calculator();// 測試正數情況(預期結果true)@ParameterizedTest(name = "測試正數 #{index} - 輸入值: {arguments}")@ValueSource(ints = {1, 2, 3, 100, Integer.MAX_VALUE}) void testIsPositive_True(int num) {assertTrue(calculator.isPositive(num),() -> "輸入值 " + num + " 應被識別為正數");}// 測試非正數情況(預期結果false)@ParameterizedTest(name = "測試非正數 #{index} - 輸入值: {arguments}")@ValueSource(ints = {-1, 0, -2, -100, Integer.MIN_VALUE})void testIsPositive_False(int num) {assertFalse(calculator.isPositive(num),() -> "輸入值 " + num + " 應被識別為非正數");}
}
示例2:多參數組合測試(@CsvSource)
測試加法方法的多組輸入輸出組合:
@ParameterizedTest(name = "測試加法 {0} + {1} = {2}")
@CsvSource({// 常規測試用例"1, 2, 3", "0, 0, 0","-1, 5, 4",// 邊界值測試用例"2147483647, 1, -2147483648", // 整數溢出情況"-2147483648, -1, 2147483647"
})
void testAddWithCsv(int a, int b, int expected) {assertEquals(expected, calculator.add(a, b),() -> String.format("%d + %d 應等于 %d", a, b, expected));
}// 更復雜的多參數組合(使用@CsvFileSource)
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
void testAddWithCsvFile(int a, int b, int expected) {// 從test-data.csv文件讀取測試數據
}
5.2 測試套件(批量執行多個測試類)
測試套件(Suit)是組織和執行多個測試類的高級方式,特別適合大型項目中的測試管理。通過測試套件可以:
- 邏輯分組相關測試類
- 按特定順序執行測試
- 過濾需要運行的測試集合
- 創建分層測試結構(套件嵌套套件)
完整實現步驟
步驟1:配置Maven依賴
<!-- 必須依賴 -->
<dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-suite-api</artifactId><version>1.9.2</version><scope>test</scope>
</dependency><dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-suite-engine</artifactId><version>1.9.2</version><scope>test</scope>
</dependency><!-- 可選:支持其他注解 -->
<dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-suite-commons</artifactId><version>1.9.2</version><scope>test</scope>
</dependency>
步驟2:創建測試套件類
import org.junit.platform.suite.api.*;// 標記為測試套件
@Suite
// 指定包含的測試類
@SelectClasses({CalculatorTest.class,StringUtilsTest.class,DatabaseTest.class
})
// 可選:包含指定包下的所有測試類
@SelectPackages("com.example.tests")
// 可選:包含/排除特定標簽的測試
@IncludeTags("fast")
@ExcludeTags("slow")
// 可選:設置執行順序
@SuiteDisplayName("核心功能測試套件")
@Order(1)
public class CoreFunctionTestSuite {// 套件類體為空,僅作為配置容器
}
高級套件配置
// 嵌套套件示例
@Suite
@SelectClasses({UnitTestSuite.class,IntegrationTestSuite.class
})
public class AllTestsSuite {}// 動態過濾測試
@Suite
@SelectPackages("com.example")
@IncludeClassNamePatterns("^.*Test$")
@ExcludeClassNamePatterns("^.*SlowTest$")
public class FilteredTestSuite {}
5.3 動態測試(運行時生成測試用例)
動態測試(Dynamic Test)是JUnit 5引入的創新特性,它允許在運行時動態生成測試用例。與靜態定義的測試方法不同,動態測試的用例可以在測試執行時根據各種條件(如外部數據源、算法結果等)即時生成。
核心組件
@TestFactory
:標記動態測試工廠方法DynamicTest
:表示單個動態測試用例DynamicContainer
:組織動態測試的分組容器
完整實現示例
基本動態測試示例
import org.junit.jupiter.api.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;class DynamicCalculatorTest {private final Calculator calculator = new Calculator();// 簡單動態測試工廠@TestFactoryStream<DynamicTest> dynamicTestsForAddition() {// 準備測試數據int[][] testCases = {{1, 1, 2},{0, 0, 0},{-1, -1, -2},{100, 200, 300}};// 生成動態測試流return Arrays.stream(testCases).map(data -> dynamicTest(data[0] + " + " + data[1] + " = " + data[2],() -> assertEquals(data[2], calculator.add(data[0], data[1]))));}
}
高級應用場景
場景1:從外部文件加載測試數據
@TestFactory
Stream<DynamicTest> generateTestsFromFile() throws IOException {// 讀取測試數據文件List<String> lines = Files.readAllLines(Paths.get("src/test/resources/test-data.csv"));return lines.stream().skip(1) // 跳過標題行.map(line -> line.split(",")).map(data -> dynamicTest("測試: " + data[0] + " + " + data[1],() -> {int a = Integer.parseInt(data[0].trim());int b = Integer.parseInt(data[1].trim());int expected = Integer.parseInt(data[2].trim());assertEquals(expected, calculator.add(a, b));}));
}
場景2:組合靜態和動態測試
@TestFactory
Collection<DynamicNode> mixedTests() {return Arrays.asList(// 靜態描述的動態測試dynamicTest("基礎加法", () -> assertEquals(2, calculator.add(1, 1))),// 動態測試容器(分組)DynamicContainer.dynamicContainer("高級運算",Stream.of(dynamicTest("大數相加", () -> assertEquals(10000, calculator.add(5000, 5000))),dynamicTest("負數相加", () -> assertEquals(-10, calculator.add(-5, -5))))),// 從方法生成的動態測試generateEdgeCaseTests());
}private List<DynamicTest> generateEdgeCaseTests() {return Arrays.asList(dynamicTest("MAX_VALUE + 1", () -> assertEquals(Integer.MIN_VALUE, calculator.add(Integer.MAX_VALUE, 1))),dynamicTest("MIN_VALUE + (-1)", () -> assertEquals(Integer.MAX_VALUE, calculator.add(Integer.MIN_VALUE, -1))));
}
動態測試的生命周期
需要注意的是,動態測試與常規測試在生命周期上的區別:
- 動態測試工廠方法(
@TestFactory
)在測試類的生命周期中執行 - 每個動態測試用例(DynamicTest)作為獨立測試執行
- 動態測試不支持
@BeforeEach
和@AfterEach
方法 - 需要通過工廠方法內部處理前置/后置邏輯
@TestFactory
Stream<DynamicTest> dynamicTestsWithSetup() {// 共享資源(在工廠方法中初始化)DatabaseTestUtil dbUtil = new DatabaseTestUtil();dbUtil.initializeTestData();return IntStream.range(0, 5).mapToObj(i -> dynamicTest("數據庫測試 #" + i, () -> {// 測試執行assertTrue(dbUtil.testRecordExists(i));// 清理(直接在測試中處理)dbUtil.cleanupAfterTest(i);}));
}
六、JUnit 與 Spring Boot 集成:實戰場景
在 Spring Boot 項目中,JUnit 已被默認集成,只需引入spring-boot-starter-test依賴,即可同時獲得 JUnit 5、Mockito(模擬依賴)等測試工具。
6.1 依賴引入(Spring Boot)
在Spring Boot項目中,要使用JUnit 5進行測試,需要在pom.xml中添加以下依賴配置:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><version>2.7.0</version> <!-- 根據實際Spring Boot版本調整 --><exclusions><!-- 排除JUnit 4依賴(如需兼容可保留) --><exclusion><groupId>junit</groupId><artifactId>junit</artifactId></exclusion></exclusions>
</dependency>
這個依賴會包含:
- JUnit 5核心庫
- Spring Test框架
- Mockito測試框架
- AssertJ斷言庫
- JSONassert庫
- Hamcrest匹配器
6.2 測試 Spring Bean(Service 層示例)
業務代碼結構
DAO層接口
// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {/*** 根據用戶名查詢用戶* @param username 用戶名* @return Optional包裝的用戶對象*/Optional<User> findByUsername(String username);
}
Service層實現
// UserService.java
@Service
@Transactional
public class UserService {@Autowiredprivate UserRepository userRepository;/*** 根據用戶名獲取用戶信息* @param username 用戶名* @return 用戶實體* @throws RuntimeException 當用戶不存在時拋出*/public User getUserByUsername(String username) {return userRepository.findByUsername(username).orElseThrow(() -> new RuntimeException("用戶不存在"));}
}
測試類實現
基礎測試類配置
// UserServiceTest.java
@ExtendWith(MockitoExtension.class) // 啟用Mockito擴展
class UserServiceTest {@Mockprivate UserRepository userRepository; // 模擬DAO層@InjectMocksprivate UserService userService; // 注入模擬對象// 測試用例...
}
測試場景1:正常查詢用戶
@Test
void testGetUserByUsername_Success() {// 1. 準備測試數據User mockUser = new User();mockUser.setId(1L);mockUser.setUsername("testUser");mockUser.setPassword("123456");// 2. 設置模擬行為when(userRepository.findByUsername("testUser")).thenReturn(Optional.of(mockUser));// 3. 執行測試方法User result = userService.getUserByUsername("testUser");// 4. 驗證結果assertNotNull(result);assertEquals("testUser", result.getUsername());assertEquals(1L, result.getId());// 5. 驗證交互行為verify(userRepository, times(1)).findByUsername("testUser");verifyNoMoreInteractions(userRepository);
}
測試場景2:查詢不存在的用戶
@Test
void testGetUserByUsername_NotExists() {// 1. 設置模擬行為when(userRepository.findByUsername("nonExistentUser")).thenReturn(Optional.empty());// 2. 驗證異常拋出RuntimeException exception = assertThrows(RuntimeException.class,() -> userService.getUserByUsername("nonExistentUser"));// 3. 驗證異常信息assertEquals("用戶不存在", exception.getMessage());// 4. 驗證交互行為verify(userRepository, times(1)).findByUsername("nonExistentUser");
}
6.3 測試Controller層(API接口測試)
基礎測試類配置
// UserControllerTest.java
@WebMvcTest(UserController.class) // 只加載Controller相關配置
@AutoConfigureMockMvc // 自動配置MockMvc
class UserControllerTest {@Autowiredprivate MockMvc mockMvc; // 模擬HTTP請求@MockBeanprivate UserService userService; // 模擬Service層// 測試用例...
}
測試GET請求
@Test
void testGetUserByUsername() throws Exception {// 1. 準備測試數據User mockUser = new User();mockUser.setId(1L);mockUser.setUsername("testUser");mockUser.setPassword("123456");// 2. 設置模擬行為when(userService.getUserByUsername("testUser")).thenReturn(mockUser);// 3. 執行并驗證HTTP請求mockMvc.perform(get("/api/users").param("username", "testUser").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$.id").value(1)).andExpect(jsonPath("$.username").value("testUser")).andExpect(jsonPath("$.password").doesNotExist()); // 敏感字段不應返回// 4. 驗證服務調用verify(userService, times(1)).getUserByUsername("testUser");
}
測試POST請求
@Test
void testCreateUser() throws Exception {// 1. 準備測試數據User newUser = new User();newUser.setUsername("newUser");newUser.setPassword("newPass");User savedUser = new User();savedUser.setId(2L);savedUser.setUsername("newUser");savedUser.setPassword("encodedPass");// 2. 設置模擬行為when(userService.createUser(any(User.class))).thenReturn(savedUser);// 3. 執行并驗證HTTP請求mockMvc.perform(post("/api/users").content(new ObjectMapper().writeValueAsString(newUser)).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isCreated()).andExpect(header().string("Location", "/api/users/2")).andExpect(jsonPath("$.id").value(2)).andExpect(jsonPath("$.username").value("newUser"));// 4. 驗證服務調用verify(userService, times(1)).createUser(any(User.class));
}
測試異常處理
@Test
void testGetUser_NotFound() throws Exception {// 1. 設置模擬行為when(userService.getUserByUsername("unknown")).thenThrow(new RuntimeException("用戶不存在"));// 2. 執行并驗證HTTP請求mockMvc.perform(get("/api/users").param("username", "unknown")).andExpect(status().isNotFound()).andExpect(jsonPath("$.message").value("用戶不存在"));
}
七、JUnit 常見問題與最佳實踐?
7.1 常見問題解決
問題 1:JUnit 5 測試方法不執行(Maven 環境)
詳細原因分析: Maven Surefire 插件是Maven項目默認使用的測試運行插件。在2.x版本中,該插件主要針對JUnit 4設計,無法自動識別JUnit 5的測試類結構(如@Test注解位于org.junit.jupiter.api包下)。這會導致Maven執行測試時跳過所有JUnit 5測試方法。
解決方案步驟:
- 在pom.xml中定位到
<build><plugins>
部分 - 添加或更新Surefire插件配置:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.0.0</version><dependencies><dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-surefire-provider</artifactId><version>1.6.2</version></dependency></dependencies>
</plugin>
? ? ?3.執行mvn clean test
驗證測試是否正常執行
典型報錯示例:
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) ---
[INFO] No tests to run.
問題 2:@BeforeAll方法報錯 "必須是靜態方法"
技術背景: JUnit 5默認采用TestInstance.Lifecycle.PER_METHOD
策略,即每個測試方法執行前都會創建新的測試類實例。因此@BeforeAll需要在類加載時就執行,必須聲明為static。
應用場景對比:
- 靜態方法場景:適合簡單的測試環境初始化,如數據庫連接池創建
- 非靜態方法場景(配合
@TestInstance
):@TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserServiceTest {private UserRepository repository; // 可注入依賴@BeforeAllvoid setupAll() { // 非靜態方法repository = new InMemoryUserRepository();} }
常見錯誤示例:
org.junit.platform.commons.JUnitException: @BeforeAll method 'void com.example.Test.setup()' must be static.
問題 3:Mockito 模擬對象為 null
框架對比說明:
場景 | Spring Boot測試 | 純JUnit測試 |
---|---|---|
注解 | @MockBean | @Mock |
初始化方式 | 自動由Spring上下文管理 | 需要手動初始化 |
典型配置 | @SpringBootTest | @ExtendWith(MockitoExtension.class) |
正確使用示例:
1.Spring Boot環境:
@SpringBootTest
class OrderServiceTest {@MockBeanprivate PaymentGateway paymentGateway; // 自動注入模擬對象@Test void test() {when(paymentGateway.process(any())).thenReturn(true);}
}
2.純JUnit環境:
@ExtendWith(MockitoExtension.class)
class CalculatorTest {@Mockprivate Random random;@Test void test() {when(random.nextInt()).thenReturn(42);}
}
7.2 最佳實踐
1. 測試方法命名規范
命名模板: [測試目標]_[測試條件]_[預期結果]
實際案例:
deposit_negativeAmount_throwIllegalArgumentException
validatePassword_lengthLessThan8_returnFalse
processOrder_outOfStockItem_triggerNotification
工具支持:
- 使用
@DisplayName
注解提供更友好的測試顯示名稱:@Test @DisplayName("當用戶名為空時應該拋出異常") void register_nullUsername_throwException() {// 測試代碼 }
2. 單一測試原則
反模式示例:
@Test
void testAdd() {// 測試正數assertEquals(5, calculator.add(2, 3));// 測試負數assertEquals(-1, calculator.add(2, -3));// 測試零值assertEquals(0, calculator.add(0, 0));
}
改進方案:
@Test
void add_twoPositives_returnSum() {...}@Test
void add_positiveAndNegative_returnDifference() {...}@Test
void add_twoZeros_returnZero() {...}
3. 避免依賴外部環境
數據庫測試方案:
# application-test.yml
spring:datasource:url: jdbc:h2:mem:testdbdriver-class-name: org.h2.Driverusername: sapassword:jpa:database-platform: org.hibernate.dialect.H2Dialect
第三方服務Mock示例:
@Test
void getWeather_withMockApi() {// 模擬天氣API返回when(weatherApi.getCurrent("Beijing")).thenReturn(new WeatherData(25, "Sunny"));WeatherReport report = service.generateReport("Beijing");assertTrue(report.contains("Sunny"));
}
4. 控制測試粒度
單元測試示例
@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {@Mockprivate UserRepository repository;@InjectMocksprivate UserService service;@Testvoid findById_existingUser() {when(repository.findById(1L)).thenReturn(Optional.of(new User()));assertNotNull(service.findUser(1L));}
}
集成測試示例
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {@Autowiredprivate MockMvc mockMvc;@Testvoid getUsers_shouldReturn200() throws Exception {mockMvc.perform(get("/api/users")).andExpect(status().isOk());}
}
5. 定期執行測試
CI配置示例(GitHub Actions):
name: Java CI
on: [push]
jobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v2- name: Set up JDKuses: actions/setup-java@v1with:java-version: '11'- name: Run testsrun: mvn test
開發流程建議:
- 本地修改代碼 → 執行相關測試
- 提交前 → 執行模塊所有測試
- 推送前 → 執行完整測試套件
- CI流水線 → 執行完整構建+測試+質量檢查