JUnit 详解
一、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流水线 → 执行完整构建+测试+质量检查