Java单元测试框架Junit5用法一览

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);
}

📊 扩展执行时序图

sequenceDiagram participant T as 测试框架 participant EP as 扩展点 participant TM as 测试方法 T->>EP: BeforeAllCallback loop 每个测试方法 T->>EP: BeforeEachCallback T->>TM: 执行测试方法 T->>EP: AfterEachCallback end T->>EP: AfterAllCallback

🔄 详细执行顺序

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. 迁移步骤

  1. 更新依赖到JUnit 5
  2. 修改注解(@Before → @BeforeEach等)
  3. 更新断言导入语句
  4. 逐步迁移测试用例

💡 最佳实践建议

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) 使用扩展机制

📋 迁移步骤

  1. 更新依赖
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>
  1. 更新导入语句
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;
  1. 迁移断言
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("================");
    }
}
相关推荐
ZouZou老师2 小时前
C++设计模式之适配器模式:以家具生产为例
java·设计模式·适配器模式
曼巴UE52 小时前
UE5 C++ 动态多播
java·开发语言
VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
程序员鱼皮2 小时前
刚刚,IDEA 免费版发布!终于不用破解了
java·程序员·jetbrains
Hui Baby3 小时前
Nacos容灾俩种方案对比
java
成富4 小时前
Chat Agent UI,类似 ChatGPT 的聊天界面,Spring AI 应用的测试工具
java·人工智能·spring·ui·chatgpt
凌波粒4 小时前
Springboot基础教程(9)--Swagger2
java·spring boot·后端
2301_800256114 小时前
【第九章知识点总结1】9.1 Motivation and use cases 9.2 Conceptual model
java·前端·数据库