JUnit 5 核心变化总结
🏗️ 架构重构:模块化设计
JUnit 5采用了全新的模块化架构,由三个核心子项目组成:
1. JUnit Platform(测试平台)
- 在JVM上启动测试框架的基础
- 支持不同测试引擎(JUnit 4、TestNG等)接入
- 定义了测试框架的API和运行时协议
🏗️ JUnit Platform
java
// 平台层提供统一的测试引擎接口
public interface TestEngine {
String getId();
TestDescriptor discover(EngineDiscoveryRequest request);
void execute(ExecutionRequest request);
}
2. JUnit Jupiter(核心引擎)
- JUnit 5的新编程模型和扩展机制
- 包含所有新特性和注解
- 是编写测试用例的主要模块
🚀 JUnit Jupiter
java
// Jupiter是新的编程模型核心
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Testable
public @interface Test {
// 新的测试注解体系
}
3. JUnit Vintage(兼容引擎)
- 向后兼容JUnit 3和JUnit 4
- 让老项目平滑迁移成为可能
🔄 JUnit Vintage
java
// 向后兼容JUnit 3/4
public class VintageTestEngine implements TestEngine {
@Override
public String getId() {
return "junit-vintage";
}
// 支持老版本测试用例执行
}
🔄 注解体系全面升级
1. 生命周期注解变化
| JUnit 4 | JUnit 5 | 作用域 |
|---|---|---|
| @Before | @BeforeEach | 每个测试方法前执行 |
| @After | @AfterEach | 每个测试方法后执行 |
| @BeforeClass | @BeforeAll | 所有测试方法前执行 |
| @AfterClass | @AfterAll | 所有测试方法后执行 |
执行顺序: BeforeAll → (BeforeEach → Test → AfterEach) × N → AfterAll
2. 新增注解
- @DisplayName:为测试类或方法提供人类可读的名称
- @Tag:支持测试分组和过滤
- @Nested:支持嵌套测试类
- @TestInstance:控制测试实例生命周期
测试组织策略
📁 测试结构最佳实践
java
// 按功能模块组织测试
@DisplayName("用户管理模块测试")
class UserManagementTest {
@Nested
@DisplayName("用户创建功能")
class UserCreationTest {
@Test
@DisplayName("创建有效用户应该成功")
void shouldCreateValidUser() {
// 测试逻辑
}
@Test
@DisplayName("创建重复用户应该失败")
void shouldFailWhenCreatingDuplicateUser() {
// 测试逻辑
}
}
@Nested
@DisplayName("用户查询功能")
class UserQueryTest {
@Test
@DisplayName("按ID查询用户")
void shouldFindUserById() {
// 测试逻辑
}
@Test
@DisplayName("查询不存在的用户")
void shouldReturnEmptyForNonExistentUser() {
// 测试逻辑
}
}
}
// 按测试类型组织
@Tag("unit")
class UnitTestSuite {
// 单元测试
}
@Tag("integration")
class IntegrationTestSuite {
// 集成测试
}
@Tag("performance")
class PerformanceTestSuite {
// 性能测试
}
🚀 高级测试特性
1. 参数化测试
支持多种数据源:
java
java
@ParameterizedTest
@ValueSource(ints = {1, 3, 5})
void testOddNumbers(int number) {
assertTrue(number % 2 == 1);
}
@ParameterizedTest
@CsvSource({"1, 2, 3", "4, 5, 9"})
void testAddition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
2. 动态测试
- 运行时生成测试用例
- 使用@TestFactory注解
- 适合数据驱动的测试场景
3. 条件测试
- @EnabledOnOs / @DisabledOnOs:操作系统条件
- @EnabledIfSystemProperty:系统属性条件
- @EnabledIfEnvironmentVariable:环境变量条件
💪 断言系统增强
1. 新的断言类
- 包名:org.junit.jupiter.api.Assertions
- 支持Lambda表达式
- 提供更丰富的断言方法
2. 断言组合
java
@Test
void groupedAssertions() {
assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
3. 异常断言改进
java
@Test
void exceptionTesting() {
Exception exception = assertThrows(IllegalArgumentException.class,
() -> methodThatThrowsException());
assertEquals("Invalid argument", exception.getMessage());
}
🔌 扩展模型(Extension Model)
1. 替代Runner机制
- 更灵活的扩展点设计
- 支持多个扩展同时使用
- 生命周期回调更精细
2. 常用扩展点
- TestInstancePostProcessor:测试实例后处理
- BeforeEachCallback / AfterEachCallback
- ParameterResolver:参数解析器
扩展点接口体系
🔌 核心扩展接口
java
// 参数解析器接口
public interface ParameterResolver {
boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext);
Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext);
}
// 测试实例后处理器
public interface TestInstancePostProcessor {
void postProcessTestInstance(Object testInstance,
ExtensionContext context);
}
// 生命周期回调接口
public interface BeforeEachCallback {
void beforeEach(ExtensionContext context);
}
public interface AfterEachCallback {
void afterEach(ExtensionContext context);
}
public interface BeforeAllCallback {
void beforeAll(ExtensionContext context);
}
public interface AfterAllCallback {
void afterAll(ExtensionContext context);
}
📊 扩展执行时序图
🔄 详细执行顺序
ruby
@ExtendWith({ExtensionA.class, ExtensionB.class, ExtensionC.class})
class ExecutionOrderTest {
// 执行顺序:
// 1. ExtensionA.beforeAll()
// 2. ExtensionB.beforeAll()
// 3. ExtensionC.beforeAll()
// 4. ExtensionA.beforeEach()
// 5. ExtensionB.beforeEach()
// 6. ExtensionC.beforeEach()
// 7. 测试方法执行
// 8. ExtensionC.afterEach()
// 9. ExtensionB.afterEach()
// 10. ExtensionA.afterEach()
// 11. ExtensionC.afterAll()
// 12. ExtensionB.afterAll()
// 13. ExtensionA.afterAll()
}
高级扩展实现模式
🎯 复合扩展模式
java
// 组合多个扩展功能的复合扩展
public class ComprehensiveExtension implements
BeforeEachCallback, AfterEachCallback, ParameterResolver, TestInstancePostProcessor {
private final RandomNumberResolver randomResolver = new RandomNumberResolver();
private final DatabaseSetupExtension dbExtension = new DatabaseSetupExtension();
private final LoggingExtension loggingExtension = new LoggingExtension();
@Override
public void beforeEach(ExtensionContext context) {
dbExtension.beforeEach(context);
loggingExtension.beforeEach(context);
}
@Override
public void afterEach(ExtensionContext context) {
loggingExtension.afterEach(context);
dbExtension.afterEach(context);
}
@Override
public boolean supportsParameter(ParameterContext paramContext,
ExtensionContext extContext) {
return randomResolver.supportsParameter(paramContext, extContext);
}
@Override
public Object resolveParameter(ParameterContext paramContext,
ExtensionContext extContext) {
return randomResolver.resolveParameter(paramContext, extContext);
}
@Override
public void postProcessTestInstance(Object instance, ExtensionContext context) {
// 自定义实例后处理逻辑
}
}
🔧 条件扩展模式
java
// 根据条件动态启用扩展
public class ConditionalExtension implements BeforeEachCallback, ExecutionCondition {
@Override
public void beforeEach(ExtensionContext context) {
// 条件性准备逻辑
if (shouldEnableExtension(context)) {
prepareTestEnvironment(context);
}
}
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if (shouldEnableExtension(context)) {
return ConditionEvaluationResult.enabled("扩展已启用");
}
return ConditionEvaluationResult.disabled("扩展被禁用");
}
private boolean shouldEnableExtension(ExtensionContext context) {
return System.getProperty("enable.extension", "false").equals("true");
}
private void prepareTestEnvironment(ExtensionContext context) {
// 环境准备逻辑
}
}
📊 JUnit 4 vs JUnit 5 对比表格
| 特性 | JUnit 4 | JUnit 5 | 改进点 |
|---|---|---|---|
| 架构 | 单一模块 | 模块化设计 | 更好的扩展性 |
| 注解 | 基础注解 | 语义化注解 | 可读性更强 |
| 断言 | 基本断言 | 丰富断言+Lambda | 表达能力更强 |
| 参数化 | 有限支持 | 多种数据源 | 测试数据管理更灵活 |
| 动态测试 | 不支持 | 原生支持 | 运行时测试生成 |
| 扩展机制 | Runner机制 | Extension模型 | 更灵活的组合 |
🛠️ 迁移指南
1. 依赖配置变化
Maven配置:
xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
2. Spring Boot集成
- Spring Boot 2.2.0+ 默认使用JUnit 5
- Spring Boot 2.4+ 移除了对Vintage的默认依赖
- 需要兼容JUnit 4时需手动引入junit-vintage-engine
3. 迁移步骤
- 更新依赖到JUnit 5
- 修改注解(@Before → @BeforeEach等)
- 更新断言导入语句
- 逐步迁移测试用例
💡 最佳实践建议
1. 测试组织
- 使用@DisplayName提高可读性
- 利用@Tag进行测试分类
- 合理使用嵌套测试组织复杂场景
2. 参数化测试应用
- 边界值测试
- 等价类划分
- 数据驱动测试场景
3. 扩展开发
- 自定义条件注解
- 测试数据准备扩展
- 测试环境管理
🎯 总结
JUnit 5是一次革命性的升级 ,不仅解决了JUnit 4的架构限制,还引入了现代化的测试理念。其模块化设计 、丰富的测试特性 和灵活的扩展机制,使得Java单元测试进入了新的时代。
主要优势:
- ✅ 更好的架构设计和扩展性
- ✅ 更丰富的测试功能和表达能力
- ✅ 与现代Java特性(Lambda等)完美结合
- ✅ 平滑的迁移路径和向后兼容
引用资料:
Junit 5 高级实战演练
高级扩展实现模式
🎯 复合扩展模式
java
// 组合多个扩展功能的复合扩展
public class ComprehensiveExtension implements
BeforeEachCallback, AfterEachCallback, ParameterResolver, TestInstancePostProcessor {
private final RandomNumberResolver randomResolver = new RandomNumberResolver();
private final DatabaseSetupExtension dbExtension = new DatabaseSetupExtension();
private final LoggingExtension loggingExtension = new LoggingExtension();
@Override
public void beforeEach(ExtensionContext context) {
dbExtension.beforeEach(context);
loggingExtension.beforeEach(context);
}
@Override
public void afterEach(ExtensionContext context) {
loggingExtension.afterEach(context);
dbExtension.afterEach(context);
}
@Override
public boolean supportsParameter(ParameterContext paramContext,
ExtensionContext extContext) {
return randomResolver.supportsParameter(paramContext, extContext);
}
@Override
public Object resolveParameter(ParameterContext paramContext,
ExtensionContext extContext) {
return randomResolver.resolveParameter(paramContext, extContext);
}
@Override
public void postProcessTestInstance(Object instance, ExtensionContext context) {
// 自定义实例后处理逻辑
}
}
🔧 条件扩展模式
java
// 根据条件动态启用扩展
public class ConditionalExtension implements BeforeEachCallback, ExecutionCondition {
@Override
public void beforeEach(ExtensionContext context) {
// 条件性准备逻辑
if (shouldEnableExtension(context)) {
prepareTestEnvironment(context);
}
}
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if (shouldEnableExtension(context)) {
return ConditionEvaluationResult.enabled("扩展已启用");
}
return ConditionEvaluationResult.disabled("扩展被禁用");
}
private boolean shouldEnableExtension(ExtensionContext context) {
return System.getProperty("enable.extension", "false").equals("true");
}
private void prepareTestEnvironment(ExtensionContext context) {
// 环境准备逻辑
}
}
参数化测试高级应用
参数源类型详解
📊 参数源对比表
| 参数源 | 注解 | 适用场景 | 示例 |
|---|---|---|---|
| 简单值 | @ValueSource |
基础数据类型测试 | @ValueSource(ints = {1, 2, 3}) |
| 枚举值 | @EnumSource |
枚举类型测试 | @EnumSource(TimeUnit.class) |
| 方法源 | @MethodSource |
复杂数据生成 | @MethodSource("dataProvider") |
| CSV数据 | @CsvSource |
多参数组合测试 | @CsvSource({"1,2,3", "4,5,9"}) |
| CSV文件 | @CsvFileSource |
外部数据文件 | @CsvFileSource(resources = "/data.csv") |
| 自定义源 | @ArgumentsSource |
复杂参数逻辑 | @ArgumentsSource(CustomProvider.class) |
高级参数化模式
🔄 动态参数生成
java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Stream;
public class AdvancedParameterizedTest {
@BeforeEach
void setUp(TestInfo info) {
//通过TestInfo获取当前测试用例信息
System.out.println("--- test : "+info.getDisplayName());
}
@ParameterizedTest
@MethodSource("generateTestData")
void testWithDynamicParameters(int input, int expected, String description) {
System.out.println("run>>> "+input+" "+expected+" "+description);
}
static Stream<Arguments> generateTestData() {
return Stream.of(
// 边界值测试
Arguments.of(Integer.MIN_VALUE, process(Integer.MIN_VALUE), "最小值测试"),
Arguments.of(-1, process(-1), "负数测试"),
Arguments.of(0, process(0), "零值测试"),
Arguments.of(1, process(1), "小正数测试"),
Arguments.of(Integer.MAX_VALUE, process(Integer.MAX_VALUE), "最大值测试"),
// 随机数据测试
Arguments.of(ThreadLocalRandom.current().nextInt(100),
process(new Random().nextInt()), "随机数测试")
);
}
private static int process(int input) {
// 模拟处理逻辑
return input * 2;
}
}
通过gralde运行用例将输出
plain
> Task :testClasses
run>>> -2147483648 0 最小值测试
run>>> -1 -2 负数测试
run>>> 0 0 零值测试
run>>> 1 2 小正数测试
run>>> 2147483647 -2 最大值测试
run>>> 17 -1658260016 随机数测试
> Task :test
AdvancedParameterizedTest > testWithDynamicParameters(int, int, String) > [1] -2147483648, 0, 最小值测试 PASSED
AdvancedParameterizedTest > testWithDynamicParameters(int, int, String) > [2] -1, -2, 负数测试 PASSED
AdvancedParameterizedTest > testWithDynamicParameters(int, int, String) > [3] 0, 0, 零值测试 PASSED
AdvancedParameterizedTest > testWithDynamicParameters(int, int, String) > [4] 1, 2, 小正数测试 PASSED
AdvancedParameterizedTest > testWithDynamicParameters(int, int, String) > [5] 2147483647, -2, 最大值测试 PASSED
AdvancedParameterizedTest > testWithDynamicParameters(int, int, String) > [6] 17, -1658260016, 随机数测试 PASSED
🎯 参数转换器
java
import cn.hutool.json.JSONUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.ArgumentConverter;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
public class CsvAndConverterParameterizedTest {
@BeforeEach
void setUp(TestInfo testInfo) {
System.out.println("==== Running: " + testInfo.getDisplayName());
}
// 使用Csv参数
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
})
void testWithCsvParameters(String name, int age) {
System.out.println(name + " is " + age + " years old");
}
// 使用自定义转换器
@ParameterizedTest
@ValueSource(strings ={"""
{"name":"John","age":30}"""
})
void testWithJsonParameters(@ConvertWith(MyArgumentConverter.class) Map<String,Object> map) {
}
// 自定义参数转换器
static public class MyArgumentConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context) {
if (source instanceof String jsonString) {
Class<?> targetType = context.getParameter().getType();
return JSONUtil.parse(jsonString)
.toBean(targetType);
}
throw new IllegalArgumentException("不支持的参数类型: " + source.getClass());
}
}
}
运行测试输出
plain
> Task :testClasses
==== Running: [1] apple, 1
apple is 1 years old
==== Running: [2] banana, 2
banana is 2 years old
==== Running: [1] {"name":"John","age":30}
> Task :test
CsvAndConverterParameterizedTest > testWithCsvParameters(String, int) > [1] apple, 1 PASSED
CsvAndConverterParameterizedTest > testWithCsvParameters(String, int) > [2] banana, 2 PASSED
CsvAndConverterParameterizedTest > testWithJsonParameters(Map) > [1] {"name":"John","age":30} PASSED
参数化测试最佳实践
✅ 数据驱动测试模式
java
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
public class DataDrivenTest {
@ParameterizedTest
//在resources目录下创建test-data.csv文件
@CsvFileSource(resources = "/test-data.csv")
void dataDrivenTest(String a, String b) {
// 读取在resources目录下创建test-data.csv文件作为参数;
// 如内容为:a,b
System.out.println("测试数据: "+a+","+b);
}
@ParameterizedTest
@ArgumentsSource(ExternalDataSource.class)
void externalDataTest(Map<String,Object> data) {
// 从外部系统获取测试数据
System.out.println("测试数据: "+data);
}
// 外部数据源
static class ExternalDataSource implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
// 从数据库、API、文件等获取数据
return fetchTestDataFromExternalSource().stream()
.map(Arguments::of);
}
private List<Map<String,Object>> fetchTestDataFromExternalSource() {
// 模拟外部数据获取
return Arrays.asList(
Map.of("a","test")
);
}
}
}
执行测试后输出
plain
> Task :testClasses
测试数据: {a=test}
测试数据: a,b
> Task :test
DataDrivenTest > externalDataTest(Map) > [1] {a=test} PASSED
DataDrivenTest > dataDrivenTest(String, String) > [1] a, b PASSED
动态测试与测试工厂
动态测试核心概念
🔄 静态测试 vs 动态测试
java
// 静态测试 - 编译时确定
@Test
void staticTest() {
// 测试逻辑在编译时固定
assertEquals(4, 2 + 2);
}
// 动态测试 - 运行时生成
@TestFactory
Stream<DynamicTest> dynamicTests() {
return Stream.of(1, 2, 3, 4, 5)
.map(number -> DynamicTest.dynamicTest(
"测试数字: " + number,
() -> {
// 测试逻辑在运行时生成
assertTrue(number > 0);
}
));
}
高级动态测试模式
🏭 测试工厂模式
java
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.stream.Stream;
public class AdvancedTestFactoryTest {
@TestFactory
Stream<DynamicNode> comprehensiveTestFactory() {
return Stream.of(
// 动态容器 - 测试分组
DynamicContainer.dynamicContainer("用户管理测试",
createUserCreationTests()),
// 动态容器 - 产品管理测试
DynamicContainer.dynamicContainer("产品管理测试",
Stream.of(
DynamicTest.dynamicTest("产品管理测试",
() -> System.out.println("产品管理测试~~~"))
)
)
);
}
private Stream<DynamicTest> createUserCreationTests() {
return Stream.of(
DynamicTest.dynamicTest("创建有效用户",
() -> System.out.println("创建有效用户测试")),
DynamicTest.dynamicTest("创建无效用户",
() -> System.out.println("创建无效用户测试"))
);
}
}
运行后输出
plain
> Task :testClasses
创建有效用户测试
创建无效用户测试
产品管理测试~~~
> Task :test
AdvancedTestFactoryTest > comprehensiveTestFactory() > 用户管理测试 > 创建有效用户 PASSED
AdvancedTestFactoryTest > comprehensiveTestFactory() > 用户管理测试 > 创建无效用户 PASSED
AdvancedTestFactoryTest > comprehensiveTestFactory() > 产品管理测试 > 产品管理测试 PASSED
动态测试生命周期
⏰ 生命周期注意事项
java
import org.junit.jupiter.api.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class DynamicTestLifecycle {
private static int setupCount = 0;
private static int tearDownCount = 0;
@BeforeEach
void beforeEach() {
setupCount++;
System.out.println("BeforeEach调用次数: " + setupCount);
}
@AfterEach
void afterEach() {
tearDownCount++;
System.out.println("AfterEach调用次数: " + tearDownCount);
}
@TestFactory
Stream<DynamicTest> dynamicTestsWithLifecycle() {
// 重要:@BeforeEach/@AfterEach只对@TestFactory方法执行一次
// 而不是对每个动态测试执行
return Stream.of(1, 2, 3)
.map(i -> DynamicTest.dynamicTest("动态测试 " + i,
() -> {
System.out.println("执行动态测试 " + i);
assertEquals(i, i);
}
));
}
// 输出结果:
// BeforeEach调用次数: 1
// 执行动态测试 1
// 执行动态测试 2
// 执行动态测试 3
// AfterEach调用次数: 1
}
条件测试与环境感知
条件测试注解详解
🌍 环境条件测试
java
class EnvironmentAwareTest {
@Test
@EnabledOnOs({OS.LINUX, OS.MAC})
void unixOnlyTest() {
// 只在Unix系统运行
assertTrue(System.getProperty("os.name").toLowerCase().contains("nix"));
}
@Test
@DisabledOnOs(OS.WINDOWS)
void nonWindowsTest() {
// 在非Windows系统运行
assertFalse(System.getProperty("os.name").toLowerCase().contains("win"));
}
@Test
@EnabledIfSystemProperty(named = "java.vendor", matches = ".*Oracle.*")
void oracleJdkTest() {
// 只在Oracle JDK运行
assertTrue(System.getProperty("java.vendor").contains("Oracle"));
}
@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
void ciEnvironmentTest() {
// 只在CI环境运行
assertEquals("true", System.getenv("CI"));
}
}
自定义条件逻辑
🔧 自定义条件注解
java
// 自定义条件注解
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(PerformanceTestCondition.class)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface EnabledOnPerformanceMode {
String value() default "performance";
}
// 条件实现
public class PerformanceTestCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
Optional<EnabledOnPerformanceMode> annotation =
context.getElement().map(e -> e.getAnnotation(EnabledOnPerformanceMode.class));
if (annotation.isPresent()) {
String mode = System.getProperty("test.mode", "default");
if (mode.equals(annotation.get().value())) {
return ConditionEvaluationResult.enabled("性能模式已启用");
}
return ConditionEvaluationResult.disabled("非性能模式,测试跳过");
}
return ConditionEvaluationResult.enabled("无性能模式限制");
}
}
// 使用自定义条件
class CustomConditionTest {
@Test
@EnabledOnPerformanceMode("performance")
void performanceTest() {
// 只在性能测试模式运行
// 执行耗时或资源密集型测试
}
@Test
@EnabledIf("customCondition")
void customConditionTest() {
// 使用自定义条件方法
assertTrue(customCondition());
}
boolean customCondition() {
// 复杂的条件逻辑
return isDatabaseAvailable() && isCacheEnabled() && hasSufficientMemory();
}
}
条件测试策略
📋 测试环境策略
java
class TestEnvironmentStrategy {
// 开发环境测试 - 快速反馈
@Test
@Tag("fast")
@EnabledIfSystemProperty(named = "env", matches = "dev")
void fastDevTest() {
// 快速执行的单元测试
}
// 集成环境测试 - 全面验证
@Test
@Tag("integration")
@EnabledIfSystemProperty(named = "env", matches = "integration")
void integrationTest() {
// 集成测试,需要外部依赖
}
// 生产环境测试 - 关键路径
@Test
@Tag("critical")
@EnabledIfSystemProperty(named = "env", matches = "production")
void productionTest() {
// 生产环境关键路径测试
}
// 性能测试 - 特定环境
@Test
@Tag("performance")
@EnabledIfEnvironmentVariable(named = "RUN_PERF_TESTS", matches = "true")
void performanceTest() {
// 性能测试,需要专用环境
}
}
测试生命周期管理
扩展上下文深度使用
🔍 ExtensionContext 详解
java
public class ContextAwareExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
// 获取测试类信息
Optional<Class<?>> testClass = context.getTestClass();
testClass.ifPresent(clazz -> {
System.out.println("测试类: " + clazz.getName());
});
// 获取测试方法信息
Optional<Method> testMethod = context.getTestMethod();
testMethod.ifPresent(method -> {
System.out.println("测试方法: " + method.getName());
// 获取方法注解
Annotation[] annotations = method.getAnnotations();
Arrays.stream(annotations)
.forEach(ann -> System.out.println("注解: " + ann.annotationType()));
});
// 获取测试实例
Optional<Object> testInstance = context.getTestInstance();
testInstance.ifPresent(instance -> {
System.out.println("测试实例: " + instance.getClass().getName());
});
// 使用存储机制共享数据
Store store = context.getStore(ExtensionContext.Namespace.create(getClass()));
store.put("startTime", System.currentTimeMillis());
}
}
存储机制高级用法
💾 命名空间策略
java
public class AdvancedStoreExtension implements BeforeEachCallback, AfterEachCallback {
// 使用不同的命名空间策略
private static final Namespace CLASS_NAMESPACE =
Namespace.create(AdvancedStoreExtension.class, "class");
private static final Namespace METHOD_NAMESPACE =
Namespace.create(AdvancedStoreExtension.class, "method");
@Override
public void beforeEach(ExtensionContext context) {
// 类级别存储 - 跨方法共享
Store classStore = context.getStore(CLASS_NAMESPACE);
if (!classStore.get("classInitialized", Boolean.class, false)) {
initializeClassResources(classStore);
classStore.put("classInitialized", true);
}
// 方法级别存储 - 方法内共享
Store methodStore = context.getStore(METHOD_NAMESPACE);
methodStore.put("methodStartTime", System.currentTimeMillis());
methodStore.put("executionCount",
methodStore.get("executionCount", Integer.class, 0) + 1);
}
@Override
public void afterEach(ExtensionContext context) {
Store methodStore = context.getStore(METHOD_NAMESPACE);
Long startTime = methodStore.get("methodStartTime", Long.class);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
System.out.println("方法执行时间: " + duration + "ms");
}
}
private void initializeClassResources(Store store) {
// 初始化类级别资源
store.put("sharedResource", createSharedResource());
}
}
测试执行监控
📊 执行监控扩展
java
public class ExecutionMonitorExtension implements
BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
private final Map<String, TestExecutionInfo> executionInfo = new ConcurrentHashMap<>();
@Override
public void beforeAll(ExtensionContext context) {
String testClass = context.getRequiredTestClass().getName();
executionInfo.put(testClass, new TestExecutionInfo());
System.out.println("开始测试类: " + testClass);
}
@Override
public void beforeEach(ExtensionContext context) {
String testMethod = context.getRequiredTestMethod().getName();
String testClass = context.getRequiredTestClass().getName();
TestExecutionInfo info = executionInfo.get(testClass);
info.startMethod(testMethod);
System.out.println("开始测试方法: " + testMethod);
}
@Override
public void afterEach(ExtensionContext context) {
String testMethod = context.getRequiredTestMethod().getName();
String testClass = context.getRequiredTestClass().getName();
TestExecutionInfo info = executionInfo.get(testClass);
info.endMethod(testMethod);
System.out.println("结束测试方法: " + testMethod);
}
@Override
public void afterAll(ExtensionContext context) {
String testClass = context.getRequiredTestClass().getName();
TestExecutionInfo info = executionInfo.get(testClass);
System.out.println("测试类 " + testClass + " 执行报告:");
info.generateReport();
}
static class TestExecutionInfo {
private final Map<String, Long> startTimes = new HashMap<>();
private final Map<String, Long> durations = new HashMap<>();
void startMethod(String methodName) {
startTimes.put(methodName, System.currentTimeMillis());
}
void endMethod(String methodName) {
Long startTime = startTimes.get(methodName);
if (startTime != null) {
durations.put(methodName, System.currentTimeMillis() - startTime);
}
}
void generateReport() {
durations.forEach((method, duration) ->
System.out.println(" " + method + ": " + duration + "ms"));
}
}
}
断言系统增强
高级断言模式
🔄 流式断言
java
class StreamAssertionsTest {
@Test
void streamAssertionsDemo() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 流式断言链
assertAll("数字集合验证",
() -> assertNotNull(numbers, "集合不应该为null"),
() -> assertFalse(numbers.isEmpty(), "集合不应该为空"),
() -> assertEquals(5, numbers.size(), "集合大小应该为5"),
() -> assertTrue(numbers.stream().allMatch(n -> n > 0), "所有数字应该大于0"),
() -> assertTrue(numbers.stream().noneMatch(n -> n > 10), "没有数字应该大于10")
);
}
@Test
void collectionAssertions() {
List<User> users = Arrays.asList(
new User(1L, "Alice", 25),
new User(2L, "Bob", 30),
new User(3L, "Charlie", 35)
);
// 集合元素断言
assertAll("用户集合验证",
() -> assertIterableEquals(
Arrays.asList("Alice", "Bob", "Charlie"),
users.stream().map(User::getName).toList(),
"用户名应该匹配"
),
() -> assertTrue(
users.stream().allMatch(u -> u.getAge() >= 18),
"所有用户应该成年"
)
);
}
}
自定义断言
🎯 领域特定断言
java
// 自定义用户断言
public class UserAssertions {
public static UserAssert assertThat(User actual) {
return new UserAssert(actual);
}
public static class UserAssert extends AbstractAssert<UserAssert, User> {
public UserAssert(User actual) {
super(actual, UserAssert.class);
}
// 验证用户是成年人
public UserAssert isAdult() {
isNotNull();
if (actual.getAge() < 18) {
failWithMessage("期望用户是成年人,但年龄是: %d", actual.getAge());
}
return this;
}
// 验证用户名格式
public UserAssert hasValidName() {
isNotNull();
if (actual.getName() == null || actual.getName().trim().isEmpty()) {
failWithMessage("用户名不应该为空");
}
if (actual.getName().length() < 2 || actual.getName().length() > 50) {
failWithMessage("用户名长度应该在2-50之间,实际是: %d",
actual.getName().length());
}
return this;
}
// 验证邮箱格式
public UserAssert hasValidEmail() {
isNotNull();
if (actual.getEmail() == null || !actual.getEmail().contains("@")) {
failWithMessage("邮箱格式不正确: %s", actual.getEmail());
}
return this;
}
}
}
// 使用自定义断言
class CustomAssertionTest {
@Test
void userValidationTest() {
User user = new User(1L, "Alice", "alice@example.com", 25);
// 链式自定义断言
UserAssert.assertThat(user)
.isAdult()
.hasValidName()
.hasValidEmail();
}
}
最佳实践与性能优化
性能优化策略
⚡ 测试执行优化
java
class PerformanceOptimizedTest {
// 使用静态资源减少重复初始化
private static ExpensiveResource sharedResource;
@BeforeAll
static void setupSharedResources() {
// 一次性初始化昂贵资源
sharedResource = new ExpensiveResource();
sharedResource.initialize();
}
@AfterAll
static void cleanupSharedResources() {
if (sharedResource != null) {
sharedResource.cleanup();
}
}
// 使用轻量级模拟对象
@Test
void fastUnitTest() {
// 使用Mock而不是真实依赖
UserService userService = mock(UserService.class);
when(userService.findById(1L)).thenReturn(Optional.of(new User(1L, "Test")));
// 快速执行,不涉及IO操作
Optional<User> user = userService.findById(1L);
assertTrue(user.isPresent());
}
// 合理使用条件测试避免不必要执行
@Test
@EnabledIfEnvironmentVariable(named = "RUN_SLOW_TESTS", matches = "true")
void slowIntegrationTest() {
// 只在需要时执行耗时测试
performSlowIntegrationTest();
}
}
测试组织策略
📁 测试结构最佳实践
java
// 按功能模块组织测试
@DisplayName("用户管理模块测试")
class UserManagementTest {
@Nested
@DisplayName("用户创建功能")
class UserCreationTest {
@Test
@DisplayName("创建有效用户应该成功")
void shouldCreateValidUser() {
// 测试逻辑
}
@Test
@DisplayName("创建重复用户应该失败")
void shouldFailWhenCreatingDuplicateUser() {
// 测试逻辑
}
}
@Nested
@DisplayName("用户查询功能")
class UserQueryTest {
@Test
@DisplayName("按ID查询用户")
void shouldFindUserById() {
// 测试逻辑
}
@Test
@DisplayName("查询不存在的用户")
void shouldReturnEmptyForNonExistentUser() {
// 测试逻辑
}
}
}
// 按测试类型组织
@Tag("unit")
class UnitTestSuite {
// 单元测试
}
@Tag("integration")
class IntegrationTestSuite {
// 集成测试
}
@Tag("performance")
class PerformanceTestSuite {
// 性能测试
}
迁移指南与常见问题
JUnit 4 到 JUnit 5 迁移
🔄 注解迁移对照表
| JUnit 4 | JUnit 5 | 迁移说明 |
|---|---|---|
@Test(expected = Exception.class) |
assertThrows(Exception.class, () -> {}) |
使用断言处理异常 |
@Test(timeout = 1000) |
assertTimeout(Duration.ofSeconds(1), () -> {}) |
使用超时断言 |
@Ignore |
@Disabled |
直接替换 |
@Category(Class) |
@Tag("tagName") |
使用标签分类 |
@RunWith(Runner.class) |
@ExtendWith(Extension.class) |
使用扩展机制 |
📋 迁移步骤
- 更新依赖
xml
<!-- 移除JUnit 4 -->
<!-- <dependency> -->
<!-- <groupId>junit</groupId> -->
<!-- <artifactId>junit</artifactId> -->
<!-- <version>4.13.2</version> -->
<!-- </dependency> -->
<!-- 添加JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
- 更新导入语句
java
// JUnit 4
import org.junit.Test;
import org.junit.Before;
// JUnit 5
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
- 迁移断言
java
// JUnit 4
import static org.junit.Assert.*;
// JUnit 5
import static org.junit.jupiter.api.Assertions.*;
常见问题与解决方案
❓ 常见问题
Q: 动态测试中@BeforeEach只执行一次? A: 这是设计行为,@BeforeEach/@AfterEach针对@TestFactory方法执行,不是每个动态测试。
Q: 参数解析器不工作? A: 检查supportsParameter方法实现,确保正确识别参数类型和注解。
Q: 扩展执行顺序不符合预期? A: 扩展执行顺序由注册顺序决定,使用@Order注解控制顺序。
Q: 条件测试在CI环境中不执行? A: 检查环境变量设置,确保条件匹配模式正确。
🔧 调试技巧
java
public class DebugExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
System.out.println("=== 调试信息 ===");
System.out.println("测试类: " + context.getRequiredTestClass().getName());
System.out.println("测试方法: " + context.getRequiredTestMethod().getName());
System.out.println("显示名称: " + context.getDisplayName());
System.out.println("标签: " + context.getTags());
System.out.println("================");
}
}