JUnit5 深度详解与完整代码实战
一、JUnit5 架构解析
JUnit5 由三大模块组成:
二、核心注解与生命周期
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 核心价值:
- 模块化架构:平台 + 引擎设计实现高度可扩展
- 现代特性支持:Lambda、参数化测试等Java 8+特性
- 灵活扩展:通过Extension API实现自定义行为
- 丰富断言:支持组合断言、异常断言等高级场景
- 测试即文档:通过
@DisplayName
和嵌套测试提高可读性推荐实践:
- 新项目直接使用JUnit5
- 结合Mockito 4+进行依赖模拟
- 使用Testcontainers进行集成测试
- 利用ArchUnit进行架构验证
- 集成Jacoco生成测试覆盖率报告
完整示例项目结构:
src/
├── main/
│ └── java/
│ └── com/
│ └── example/
│ ├── service/
│ ├── repository/
│ └── model/
└── test/
└── java/
└── com/
└── example/
├── unit/
├── integration/
└── component/