JUnit5 深度详解与完整代码实战

JUnit5 深度详解与完整代码实战

一、JUnit5 架构解析

JUnit5 由三大模块组成:

运行基础
启动
JUnit Platform
TestEngine API
Launcher API
Jupiter Engine
Vintage Engine
IDE集成
构建工具
持续集成
支持新特性
兼容JUnit4

二、核心注解与生命周期

1. 基础注解
import org.junit.jupiter.api.*;

class LifecycleTest {

    @BeforeAll
    static void setupAll() {
        System.out.println("所有测试前执行一次");
    }

    @BeforeEach
    void setupEach() {
        System.out.println("每个测试前执行");
    }

    @Test
    void firstTest() {
        System.out.println("测试用例1");
    }

    @Test
    @DisplayName("自定义测试名称")
    void secondTest() {
        System.out.println("测试用例2");
    }

    @AfterEach
    void tearDownEach() {
        System.out.println("每个测试后执行");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("所有测试后执行一次");
    }
}
2. 测试生命周期执行顺序
所有测试前执行一次
每个测试前执行
测试用例1
每个测试后执行
每个测试前执行
自定义测试名称
每个测试后执行
所有测试后执行一次

三、断言机制详解

1. 基础断言
import static org.junit.jupiter.api.Assertions.*;

@Test
void basicAssertions() {
    // 相等断言
    assertEquals(4, 2 * 2, "乘法计算错误");
    
    // 非空断言
    assertNotNull(new Object(), "对象应为非空");
    
    // 条件断言
    assertTrue(5 > 3, "条件应为真");
    
    // 异常断言
    Throwable ex = assertThrows(ArithmeticException.class, () -> {
        int i = 1 / 0;
    });
    assertEquals("/ by zero", ex.getMessage());
}
2. 高级断言
@Test
void advancedAssertions() {
    // 组合断言(所有断言都会执行)
    assertAll("用户信息校验",
        () -> assertEquals("John", user.getFirstName()),
        () -> assertEquals("Doe", user.getLastName()),
        () -> assertTrue(user.isActive())
    );
    
    // 超时断言
    assertTimeout(Duration.ofMillis(100), () -> {
        Thread.sleep(50);
    });
    
    // 提前失败
    if (user == null) {
        fail("用户对象不应为空");
    }
}

四、参数化测试

1. 基础参数化
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, 7})
void isOdd_ShouldReturnTrue(int number) {
    assertTrue(number % 2 != 0);
}
2. 多参数测试
@ParameterizedTest
@CsvSource({
    "apple, 1",
    "banana, 2",
    "orange, 3"
})
void testWithCsv(String fruit, int rank) {
    assertNotNull(fruit);
    assertTrue(rank > 0);
}

@ParameterizedTest
@MethodSource("provideStrings")
void testWithMethodSource(String input) {
    assertTrue(input.length() > 3);
}

private static Stream<Arguments> provideStrings() {
    return Stream.of(
        Arguments.of("Java"),
        Arguments.of("Kotlin"),
        Arguments.of("Scala")
    );
}
3. 枚举参数
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnum(TimeUnit timeUnit) {
    assertNotNull(timeUnit);
}

五、嵌套测试

@DisplayName("银行账户测试")
class BankAccountTest {
    private BankAccount account;

    @BeforeEach
    void createAccount() {
        account = new BankAccount(1000);
    }

    @Nested
    @DisplayName("存款操作")
    class DepositTests {
        @Test
        @DisplayName("正常存款")
        void depositPositive() {
            account.deposit(500);
            assertEquals(1500, account.getBalance());
        }

        @Test
        @DisplayName("存款负数应失败")
        void depositNegative() {
            assertThrows(IllegalArgumentException.class, () -> account.deposit(-100));
        }
    }

    @Nested
    @DisplayName("取款操作")
    class WithdrawTests {
        @Test
        @DisplayName("正常取款")
        void withdrawPositive() {
            account.withdraw(500);
            assertEquals(500, account.getBalance());
        }

        @Test
        @DisplayName("余额不足")
        void withdrawInsufficient() {
            assertThrows(InsufficientFundsException.class, () -> account.withdraw(1500));
        }
    }
}

六、动态测试

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
    List<String> inputList = Arrays.asList("Java", "Kotlin", "Scala");
    
    return inputList.stream()
        .map(lang -> DynamicTest.dynamicTest("测试语言: " + lang, 
            () -> assertTrue(lang.length() > 3)));
}

@TestFactory
Collection<DynamicTest> generateDynamicTests() {
    return Arrays.asList(
        dynamicTest("加法测试", () -> assertEquals(4, 2 + 2)),
        dynamicTest("乘法测试", () -> assertEquals(6, 3 * 2))
    );
}

七、条件测试

@Test
@EnabledOnOs(OS.MAC)
void onlyOnMac() {
    // 仅在Mac系统执行
}

@Test
@DisabledOnJre(JRE.JAVA_8)
void notOnJava8() {
    // 不在Java 8环境执行
}

@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "dev")
void onlyInDevEnv() {
    // 仅在开发环境执行
}

@Test
@EnabledIf("customCondition")
void basedOnCustomCondition() {
    // 基于自定义条件执行
}

boolean customCondition() {
    return LocalTime.now().getHour() < 18; // 仅在18点前执行
}

八、扩展模型

1. 自定义扩展
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    
    private static final String START_TIME = "start_time";
    
    @Override
    public void beforeTestExecution(ExtensionContext context) {
        getStore(context).put(START_TIME, System.currentTimeMillis());
    }
    
    @Override
    public void afterTestExecution(ExtensionContext context) {
        long startTime = getStore(context).remove(START_TIME, long.class);
        long duration = System.currentTimeMillis() - startTime;
        System.out.printf("测试 %s 耗时 %d ms%n", 
            context.getDisplayName(), duration);
    }
    
    private ExtensionContext.Store getStore(ExtensionContext context) {
        return context.getStore(ExtensionContext.Namespace.create(
            getClass(), context.getRequiredTestMethod()));
    }
}

@ExtendWith(TimingExtension.class)
class TimingTests {
    @Test
    void sleep20ms() throws Exception {
        Thread.sleep(20);
    }
}
2. Mockito 集成
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void getUserById() {
        when(userRepository.findById(1L)).thenReturn(new User(1L, "John"));
        
        User user = userService.getUserById(1L);
        
        assertEquals("John", user.getName());
        verify(userRepository).findById(1L);
    }
}

九、测试执行顺序

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTests {
    
    @Test
    @Order(3)
    void thirdTest() {
        System.out.println("第三个执行");
    }
    
    @Test
    @Order(1)
    void firstTest() {
        System.out.println("第一个执行");
    }
    
    @Test
    @Order(2)
    void secondTest() {
        System.out.println("第二个执行");
    }
}

十、测试接口

interface LoggingExtension {
    @BeforeEach
    default void logStart(TestInfo info) {
        System.out.println("开始测试: " + info.getDisplayName());
    }
    
    @AfterEach
    default void logEnd(TestInfo info) {
        System.out.println("结束测试: " + info.getDisplayName());
    }
}

class InterfaceTests implements LoggingExtension {
    @Test
    void testWithInterface() {
        assertEquals(4, 2 + 2);
    }
}

十一、测试模板

@RepeatedTest(3)
void repeatedTest(RepetitionInfo repetitionInfo) {
    System.out.println("执行次数: " + repetitionInfo.getCurrentRepetition());
    assertEquals(2, 1 + 1);
}

@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String parameter) {
    assertNotNull(parameter);
}

static class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {
    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return true;
    }

    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
            ExtensionContext context) {
        return Stream.of("param1", "param2", "param3")
            .map(parameter -> invocationContext(parameter));
    }
    
    private TestTemplateInvocationContext invocationContext(String parameter) {
        return new TestTemplateInvocationContext() {
            @Override
            public String getDisplayName(int invocationIndex) {
                return "参数: " + parameter;
            }
            
            @Override
            public List<Extension> getAdditionalExtensions() {
                return Collections.emptyList();
            }
        };
    }
}

十二、最佳实践

1. 测试结构
class UserServiceTest {
    // 测试准备
    @Mock UserRepository repository;
    @InjectMocks UserService service;
    
    // 测试用例
    @Test
    void createUser_ValidInput_ReturnsUser() {
        // Arrange
        UserDto dto = new UserDto("john@example.com", "John");
        
        // Act
        User result = service.createUser(dto);
        
        // Assert
        assertNotNull(result.getId());
        assertEquals("John", result.getName());
    }
    
    @Test
    void createUser_DuplicateEmail_ThrowsException() {
        // Arrange
        when(repository.existsByEmail("dupe@mail.com")).thenReturn(true);
        
        // Act & Assert
        assertThrows(DuplicateEmailException.class, () -> {
            service.createUser(new UserDto("dupe@mail.com", "Duplicate"));
        });
    }
}
2. 测试配置
<!-- Maven 配置 -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.0</version>
    <scope>test</scope>
</dependency>

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M7</version>
        </plugin>
    </plugins>
</build>
// Gradle 配置
test {
    useJUnitPlatform()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0'
    testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1'
}

十三、高级特性

1. 并行测试
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent
2. 测试报告
@ExtendWith(TestReportExtension.class)
class ReportTests {
    // 测试执行后将生成HTML报告
}

class TestReportExtension implements Extension {
    // 实现报告生成逻辑
}

十四、常见问题解决

问题解决方案
@BeforeAll 非静态方法添加 static 修饰符
迁移后找不到 @Test导入 org.junit.jupiter.api.Test
Mockito 注入失败添加 @ExtendWith(MockitoExtension.class)
参数化测试不执行检查参数提供方法是否为静态
测试未运行确认使用 JUnit Platform

JUnit5 核心价值

  1. 模块化架构:平台 + 引擎设计实现高度可扩展
  2. 现代特性支持:Lambda、参数化测试等Java 8+特性
  3. 灵活扩展:通过Extension API实现自定义行为
  4. 丰富断言:支持组合断言、异常断言等高级场景
  5. 测试即文档:通过@DisplayName和嵌套测试提高可读性

推荐实践

  • 新项目直接使用JUnit5
  • 结合Mockito 4+进行依赖模拟
  • 使用Testcontainers进行集成测试
  • 利用ArchUnit进行架构验证
  • 集成Jacoco生成测试覆盖率报告

完整示例项目结构:

src/
├── main/
│   └── java/
│       └── com/
│           └── example/
│               ├── service/
│               ├── repository/
│               └── model/
└── test/
    └── java/
        └── com/
            └── example/
                ├── unit/
                ├── integration/
                └── component/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值