第一章:JUnit 入门基础
1.1 JUnit 简介与版本演进
本小节介绍 JUnit 的基本定位及其主要版本差异。
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| JUnit | Java 语言中最广泛使用的单元测试框架,用于验证代码逻辑的正确性。 | 需配合构建工具(如 Maven/Gradle)和 IDE 使用。 |
| JUnit 3 | 基于继承 TestCase 类,使用命名约定(如 testXxx())识别测试方法。 | 已淘汰,不推荐新项目使用。 |
| JUnit 4 | 引入注解(如 @Test),支持参数化测试、假设(Assume)等,依赖 Java 5+。 | 仍被部分老项目使用,但官方已停止主要维护。 |
| JUnit 5 (Jupiter) | 模块化架构(Platform + Jupiter + Vintage),支持 Lambda、动态测试、扩展模型等,需 Java 8+。 | 当前主流版本,推荐新项目采用。 |
| JUnit Platform | JUnit 5 的底层测试引擎,支持其他测试框架(如 Spek、Kotest)在其上运行。 | 开发者通常无需直接操作 Platform。 |
| JUnit Vintage | 用于在 JUnit 5 环境中运行 JUnit 3/4 的测试用例。 | 仅在迁移旧测试时需要引入。 |
1.2 环境搭建与依赖配置
本小节说明如何在 Maven 和 Gradle 项目中配置 JUnit 5。
Maven 项目配置步骤
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 添加 JUnit Jupiter API 依赖 | 在 pom.xml 中添加: | 版本建议使用最新稳定版(如 5.10.0)。scope 必须为 test。 |
| 添加 Surefire 插件(可选但推荐) | 在 pom.xml 中添加: | 若未配置,Maven 可能无法自动发现 JUnit 5 测试。 |
| 验证 Java 版本 | 确保项目 JDK ≥ 8 | JUnit 5 不支持 Java 7 及以下。 |
Maven 依赖配置示例:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Maven Surefire 插件配置示例:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M9</version>
</plugin>
Gradle 项目配置步骤
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 应用 Java 插件 | build.gradle 中包含:apply plugin: 'java' 或 plugins { id 'java' } | 必须启用 Java 插件才能编译测试代码。 |
| 添加 JUnit Jupiter 依赖 | 在 dependencies 块中添加: | 使用 testImplementation 而非 testCompile(后者已废弃)。 |
| 启用 JUnit Platform | 在 build.gradle 中添加: | 若省略此配置,Gradle 默认使用 JUnit 4,导致测试不执行。 |
Gradle 配置示例:
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
test {
useJUnitPlatform()
}
1.3 第一个 JUnit 测试用例
本小节展示如何编写并运行最简单的 JUnit 5 测试类。
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@Test 注解方法 | public void methodName() { ... } | 标识一个测试方法 | 见下方代码块 | 方法必须是 non-private、non-static;返回类型必须为 void;建议使用 void 而非 public(JUnit 5 支持 package-private)。 |
Assertions.assertEquals | assertEquals(expected, actual) | 断言两个值相等 | assertEquals(10, 5 + 5); | 若 expected 与 actual 不等,测试失败并抛出 AssertionError。 |
| 运行测试(IDE 方式) | 在 IDE 中右键测试类 → Run ‘CalculatorTest’ | 执行测试用例 | —— | 需确保 IDE 已正确识别 JUnit 5(IntelliJ IDEA 2019.2+ / Eclipse 2018-12+ 原生支持)。 |
| 运行测试(命令行) | mvn test(Maven)或 ./gradlew test(Gradle) | 通过构建工具批量执行 | —— | 需先完成 1.2 节的依赖与插件配置,否则可能跳过测试。 |
测试类示例:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
void testAdd() {
assertEquals(4, 2 + 2);
}
}
第二章:核心注解与生命周期
2.1 @Test 注解详解
@Test 是 JUnit 5 中标识测试方法的核心注解。
| 方法/注解名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@Test | 在方法上标注 @Test | 标识一个独立的测试用例方法 | 见下方代码块 | 方法不能是 private 或 static;返回类型必须为 void;可抛出任何异常(但未捕获的异常会导致测试失败)。 |
| 自定义显示名称 | @Test + @DisplayName("自定义名称") | 在测试报告或 IDE 中显示更友好的名称 | @DisplayName("加法运算应返回正确结果") | @DisplayName 支持空格、中文、表情符号等,仅用于展示,不影响执行。 |
| 测试方法命名建议 | 使用描述性名称,如 shouldDoXxxWhenYyy() | 提高可读性和可维护性 | void shouldThrowExceptionWhenDivideByZero() { ... } | 避免使用 testXxx 命名(JUnit 3 风格),推荐行为驱动命名。 |
@Test 基本使用示例:
import org.junit.jupiter.api.Test;
class ExampleTest {
@Test
void shouldReturnTrue() {
assert true;
}
}
@DisplayName 使用示例:
@Test
@DisplayName("加法运算应返回正确结果")
void testAddition() {
assertEquals(4, 2 + 2);
}
2.2 生命周期注解(@BeforeEach, @AfterEach, @BeforeAll, @AfterAll)
这些注解控制测试类中方法的执行时机,用于初始化和清理资源。
| 注解名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@BeforeEach | 在 non-static 方法上标注 @BeforeEach | 每个 @Test 执行前运行一次 | 见下方代码块 | 方法必须是非静态的;可有多个 @BeforeEach 方法(执行顺序不确定,不推荐)。 |
@AfterEach | 在 non-static 方法上标注 @AfterEach | 每个 @Test 执行后运行一次 | 见下方代码块 | 用于释放资源(如关闭连接、删除临时文件);即使测试失败也会执行。 |
@BeforeAll | 在 static 方法上标注 @BeforeAll | 整个测试类开始前运行一次 | 见下方代码块 | 方法必须是 static(或配合 @TestInstance(Lifecycle.PER_CLASS) 可为非静态);用于昂贵的一次性初始化(如启动服务器)。 |
@AfterAll | 在 static 方法上标注 @AfterAll | 整个测试类结束后运行一次 | 见下方代码块 | 同样需为 static(除非使用 PER_CLASS 实例生命周期);即使部分测试失败也会执行。 |
@TestInstance(Lifecycle.PER_CLASS) | 在测试类上标注 | 改变测试实例生命周期为”每个类一个实例” | 见下方代码块 | 默认是 PER_METHOD(每个测试方法新建实例);PER_CLASS 可提升性能,但需注意状态污染风险。 |
完整生命周期示例:
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll
static void init() {
System.setProperty("env", "test");
}
@BeforeEach
void setUp() {
database = new InMemoryDB();
database.connect();
}
@AfterEach
void tearDown() {
if (database != null) database.close();
}
@AfterAll
static void cleanup() {
TempDir.deleteAll();
}
@Test
void testSomething() {
// ...
}
}
PER_CLASS 生命周期示例:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyTest {
@BeforeAll
void init() { ... } // 可为非静态
}
2.3 禁用与条件执行(@Disabled, @EnabledOnOs 等)
JUnit 5 提供多种条件注解,用于在特定环境下启用或跳过测试。
| 注解名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@Disabled | @Disabled 或 @Disabled("原因说明") | 永久禁用某个测试(相当于注释掉) | @Disabled("等待修复 #123") | 被禁用的测试不会执行,但在报告中标记为”skipped”。 |
@EnabledOnOs | @EnabledOnOs({OS.WINDOWS}) | 仅在指定操作系统上运行 | @EnabledOnOs(OS.LINUX) | OS 枚举包括 WINDOWS、LINUX、MAC、OTHER;多系统用数组。 |
@DisabledOnOs | @DisabledOnOs(OS.MAC) | 在指定操作系统上跳过测试 | @DisabledOnOs(OS.MAC) | 与 @EnabledOnOs 互斥,避免同时使用。 |
@EnabledOnJre | @EnabledOnJre(JRE.JAVA_17) | 仅在指定 Java 版本运行 | @EnabledOnJre(JRE.JAVA_11) | JRE 枚举包括 JAVA_8 到 JAVA_21 等;适用于验证版本兼容性。 |
@DisabledOnJre | @DisabledOnJre(JRE.JAVA_8) | 在指定 Java 版本跳过 | @DisabledOnJre(JRE.JAVA_8) | —— |
@EnabledIfSystemProperty | @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*") | 根据系统属性启用 | 见下方代码块 | matches 使用正则表达式;若属性不存在,测试被禁用。 |
@EnabledIfEnvironmentVariable | @EnabledIfEnvironmentVariable(named = "ENV", matches = "prod") | 根据环境变量启用 | @EnabledIfEnvironmentVariable(named = "CI", matches = "true") | 适用于 CI/CD 环境中的特殊测试。 |
@EnabledIf / @DisabledIf(自定义) | @EnabledIf("customCondition") | 通过自定义逻辑控制 | 见下方代码块 | 条件方法必须是 static boolean,且无参数;慎用,避免引入外部依赖导致不可靠。 |
条件执行示例:
@Test
@EnabledOnOs(OS.LINUX)
void linuxOnlyTest() { ... }
@Test
@DisabledOnOs(OS.MAC)
void nonMacTest() { ... }
@Test
@EnabledIfSystemProperty(named = "user.country", matches = "CN")
void chinaSpecificTest() { ... }
// 自定义条件
static boolean isFastNetwork() {
return Network.speed() > 100;
}
@Test
@EnabledIf("isFastNetwork")
void fastNetworkTest() { ... }
第三章:断言机制(Assertions)
3.1 基本断言方法(assertEquals, assertTrue 等)
这些是最常用的断言方法,用于验证逻辑结果是否符合预期。
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
assertEquals | assertEquals(expected, actual) / assertEquals(expected, actual, message) | 断言两个值相等(使用 equals() 比较) | assertEquals(5, 2 + 3); | 对于浮点数,应使用带 delta 的重载:assertEquals(0.1, result, 0.001);message 支持 Lambda 表达式(延迟求值)。 |
assertNotEquals | assertNotEquals(unexpected, actual) | 断言两个值不相等 | assertNotEquals(0, list.size()); | 同样使用 equals() 判断不等。 |
assertTrue | assertTrue(condition) / assertTrue(condition, message) | 断言条件为 true | assertTrue(user.isActive()); | message 推荐使用 Lambda 避免不必要的字符串拼接开销。 |
assertFalse | assertFalse(condition) | 断言条件为 false | assertFalse(str.isBlank()); | —— |
assertNull | assertNull(actual) | 断言对象为 null | assertNull(cache.get("invalid_key")); | —— |
assertNotNull | assertNotNull(actual) / assertNotNull(actual, message) | 断言对象非 null | assertNotNull(result, "计算结果不应为 null"); | 常用于验证工厂方法或解析器返回值。 |
assertSame | assertSame(expected, actual) | 断言两个引用指向同一对象(== 比较) | assertSame(singletonInstance, getInstance()); | 适用于单例、缓存等场景。 |
assertNotSame | assertNotSame(unexpected, actual) | 断言两个引用不指向同一对象 | assertNotSame(new Object(), obj); | —— |
基本断言使用示例:
assertEquals(5, 2 + 3);
assertEquals("Hello", str.trim(), "字符串应无前后空格");
assertTrue(list.isEmpty(), () -> "列表应为空,但实际大小为:" + list.size());
assertFalse(str.isBlank());
assertNull(cache.get("invalid_key"));
assertNotNull(result, "计算结果不应为 null");
3.2 异常断言(assertThrows, assertDoesNotThrow)
用于验证代码是否按预期抛出(或不抛出)异常。
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
assertThrows | assertThrows(expectedType, executable) | 断言执行代码块时抛出指定类型的异常 | 见下方代码块 | executable 是 ThrowingRunnable(无参无返回 Lambda);必须捕获返回的异常以进一步验证消息或原因。 |
assertDoesNotThrow | assertDoesNotThrow(executable) / assertDoesNotThrow(executable, message) | 断言代码块不抛出任何异常 | 见下方代码块 | 若需获取返回值,可将结果赋给变量;适用于验证”正常路径”无异常。 |
assertThrows 与泛型 | 可配合泛型精确匹配子类异常 | assertThrows(FileNotFoundException.class, () -> new FileInputStream("missing.txt")); | 不要使用父类(如 Exception)断言,会降低测试精度。 | 若抛出的是子类异常但断言父类,测试仍通过,但可能掩盖问题。 |
异常断言示例:
// assertThrows - 验证异常类型和消息
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> calculator.divide(10, 0)
);
assertEquals("除数不能为零", ex.getMessage());
// assertDoesNotThrow - 验证不抛异常并获取返回值
String result = assertDoesNotThrow(
() -> service.process(input)
);
3.3 超时断言(assertTimeout)
用于确保代码在指定时间内完成执行。
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
assertTimeout | assertTimeout(Duration timeout, executable) | 断言代码在超时前完成(在当前线程执行) | 见下方代码块 | executable 可返回值;若超时,抛出 AssertionFailedError;任务在主线程运行,可访问局部变量。 |
assertTimeoutPreemptively | assertTimeoutPreemptively(Duration timeout, executable) | 断言代码在超时前完成(在独立线程执行,超时可中断) | 见下方代码块 | 适用于可能死锁或无限循环的代码;但因跨线程,无法访问外部非 final 变量;中断不保证立即生效。 |
Duration 构造 | Duration.ofSeconds(1), Duration.ofMillis(100) | 创建时间间隔 | Duration.ofMinutes(1) | 使用 java.time.Duration,避免使用毫秒整数(易错)。 |
超时断言示例:
// 在主线程执行,超时抛 AssertionFailedError
assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(1000);
return heavyComputation();
});
// 在独立线程执行,可中断死循环
assertTimeoutPreemptively(Duration.ofMillis(500), () -> {
while (true) { /* 死循环 */ }
});
3.4 组合断言(assertAll)
允许在一个测试中执行多个断言,并在最后统一报告所有失败(而非第一个失败就终止)。
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
assertAll | assertAll(Stream<Executable> executables) / assertAll(String heading, Executable...) | 批量执行断言,收集所有失败 | 见下方代码块 | 每个断言必须包装为 Executable(即无参无返回 Lambda);heading 用于错误报告分组;即使部分失败,其余断言仍会执行。 |
嵌套 assertAll | 在 assertAll 内部再调用 assertAll | 构建层次化断言结构 | 见下方代码块 | 有助于组织复杂对象的验证逻辑;错误信息更具结构性。 |
| 与异常结合 | assertAll 中可包含 assertThrows | 组合验证 | 见下方代码块 | 所有 Executable 均可包含任意断言逻辑;但注意异常断言本身不抛出异常(它验证异常是否被抛出)。 |
组合断言示例:
// 基本组合断言
assertAll(
"用户信息验证",
() -> assertEquals("张三", user.getName()),
() -> assertEquals(25, user.getAge()),
() -> assertTrue(user.isVerified())
);
// 嵌套 assertAll
assertAll(
"地址验证",
() -> assertAll("省市区",
() -> assertNotNull(addr.province),
() -> assertNotNull(addr.city)
),
() -> assertNotNull(addr.street)
);
// 与异常结合
assertAll(
() -> assertEquals(1, list.size()),
() -> assertThrows(NullPointerException.class, () -> list.add(null))
);
第四章:参数化测试(Parameterized Tests)
4.1 @ParameterizedTest 基础用法
@ParameterizedTest 是 JUnit 5 中实现数据驱动测试的核心注解,允许一个测试方法被多次调用,每次使用不同参数。
| 注解/概念名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@ParameterizedTest | 在方法上标注 @ParameterizedTest,并配合参数源注解 | 标识一个参数化测试方法 | 见下方代码块 | 方法必须声明与参数源匹配的参数列表;不能与 @Test 同时使用;需引入 junit-jupiter-params 模块(Maven/Gradle 默认包含在 junit-jupiter 中)。 |
| 显示名称模板 | @ParameterizedTest(name = "[{index}] {arguments}") | 自定义每次测试运行的显示名称 | @ParameterizedTest(name = "输入 {0} 应为偶数") | 占位符:{index}:从 1 开始的序号;{arguments}:全部参数;{0}, {1}…:单个参数。 |
| 测试方法签名 | void methodName(Type param1, Type param2, ...) | 接收来自参数源的数据 | 见下方代码块 | 参数数量和类型必须与参数源严格匹配;支持基本类型、String、枚举、对象等。 |
@ParameterizedTest 基本示例:
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testIsPositive(int n) {
assertTrue(n > 0);
}
自定义显示名称示例:
@ParameterizedTest(name = "输入 {0} 应为偶数")
@ValueSource(ints = {2, 4, 6})
void testEven(int n) { ... }
多参数签名示例:
@ParameterizedTest
@CsvSource({"a,1", "b,2"})
void testPair(String s, int i) { ... }
4.2 参数来源(@ValueSource, @CsvSource, @MethodSource 等)
JUnit 5 提供多种内置注解作为参数来源,满足不同场景需求。
| 参数源注解 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@ValueSource | @ValueSource(ints = {1,2}) / @ValueSource(strings = {"a","b"}) / @ValueSource(classes = {String.class}) | 提供单一类型的一维数组字面量 | @ValueSource(strings = {"hello", "world"}) | 支持类型:shorts, bytes, ints, longs, floats, doubles, chars, booleans, strings, classes;不支持对象或复杂结构。 |
@EnumSource | @EnumSource(TimeUnit.class) / @EnumSource(names = {"SECONDS", "MINUTES"}) | 使用枚举常量作为参数 | @EnumSource(TimeUnit.class) | 可通过 names 或 mode 过滤(如 EXCLUDE);适用于覆盖所有枚举值的测试。 |
@CsvSource | @CsvSource({"1, a", "2, b"}) / @CsvSource(value = "foo; bar", delimiter = ';') | 从 CSV 格式字符串提供多参数组合 | @CsvSource({"3, odd", "4, even"}) | 默认逗号分隔;空值用 null 表示(如 "1, null");支持转义引号;每行对应一次测试调用。 |
@CsvFileSource | @CsvFileSource(resources = "/data.csv") | 从类路径下的 CSV 文件读取参数 | @CsvFileSource(resources = "/test-cases.csv", numLinesToSkip = 1) | resources 路径相对于 src/test/resources;numLinesToSkip 可跳过标题行;文件编码默认 UTF-8。 |
@MethodSource | @MethodSource("methodName") | 从本地或外部静态方法获取参数流 | 见下方代码块 | 方法必须返回 Stream、Iterable、Iterator 或数组;可位于当前类或指定类(如 "com.example.TestData#values");方法必须是 static。 |
@ArgumentsSource | @ArgumentsSource(MyProvider.class) | 使用自定义 ArgumentsProvider 实现 | 见下方代码块 | 适用于复杂参数生成逻辑;需实现 ArgumentsProvider 接口;比 @MethodSource 更灵活但更冗长。 |
@MethodSource 示例:
@ParameterizedTest
@MethodSource("provideStrings")
void testWithMethodSource(String s) { ... }
static Stream<String> provideStrings() {
return Stream.of("x", "y");
}
@ArgumentsSource 示例:
@ParameterizedTest
@ArgumentsSource(RangeProvider.class)
void testRange(int n) { ... }
static class RangeProvider implements ArgumentsProvider {
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return IntStream.range(0, 5).mapToObj(Arguments::of);
}
}
4.3 自定义参数转换器(ArgumentConverter)
当参数源提供的原始数据类型与测试方法参数类型不匹配时,可通过自定义转换器进行转换。
| 概念/注解名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@ConvertWith | @ParameterizedTest + @ValueSource(strings = {"2023-01-01"}) | 指定单个参数的转换器 | 见下方完整示例 | 仅作用于标注的参数;转换器必须实现 ArgumentConverter 接口。 |
ArgumentConverter | public class LocalDateConverter implements ArgumentConverter | 将原始参数(如 String)转换为目标类型(如 LocalDate) | 见下方完整示例 | 转换器方法必须 public;若转换失败应抛出异常(测试将失败);不支持泛型自动推导,需显式指定。 |
| 全局转换器(通过扩展) | 通过注册全局 ArgumentConverter 扩展 | 对所有匹配类型的参数自动转换 | 需实现 ParameterResolver 和 ArgumentConverter 并注册为 Extension | JUnit 5 默认不支持全局自动转换;通常推荐使用 @ConvertWith 显式声明,提高可读性。 |
| 内置转换支持 | JUnit 5 自动支持部分类型转换(如 String → enum, primitive ↔ wrapper) | 无需自定义转换器 | @ValueSource(strings = {"RED", "BLUE"}) + void testColor(Color c) { ... }(Color 为枚举) | 自动转换有限;复杂类型(如 JSON 字符串 → POJO)仍需自定义转换器。 |
自定义转换器完整示例:
class DateTest {
@ParameterizedTest
@ValueSource(strings = {"2023-01-01", "2024-12-31"})
void testHoliday(@ConvertWith(LocalDateConverter.class) LocalDate date) {
assertNotNull(date);
}
static class LocalDateConverter implements ArgumentConverter {
public Object convert(Object source, ParameterContext context) {
if (source instanceof String) {
return LocalDate.parse((String) source);
}
throw new IllegalArgumentException("无法转换");
}
}
}
第五章:测试套件与分组
5.1 @Suite 与测试套件构建
JUnit 5 通过 @Suite 注解和相关选择器注解,支持将多个测试类组合为一个逻辑测试套件,便于批量执行。
| 注解/概念名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@Suite | 在空类上标注 @Suite 及选择器注解 | 定义一个测试套件入口类 | 见下方代码块 | 套件类本身不包含测试方法;必须配合至少一个选择器注解(如 @SelectClasses);需引入 junit-platform-suite 引擎依赖。 |
| 所需依赖(Maven) | —— | 提供 @Suite 支持 | 见下方代码块 | 若未添加此依赖,运行套件会报 ClassNotFoundException 或被忽略。 |
| 所需依赖(Gradle) | testImplementation 'org.junit.platform:junit-platform-suite:1.10.0' | 同上 | —— | 同样需在 build.gradle 中显式声明。 |
| 套件执行方式 | IDE:右键运行套件类 / Maven:mvn test -Dtest=ServiceTestSuite / Gradle:./gradlew test --tests "ServiceTestSuite" | 执行整个套件中的所有测试 | —— | 套件类名需符合构建工具的测试类命名规则(通常含 Test/Suite)。 |
| 嵌套套件 | 不支持直接嵌套 @Suite | 无法在一个套件中包含另一个套件类 | —— | 可通过标签(@Tag)或包结构间接实现分层组织。 |
@Suite 套件示例:
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
@Suite
@SelectClasses({UserServiceTest.class, OrderServiceTest.class})
public class ServiceTestSuite {
// 空类即可
}
Maven 依赖配置:
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.10.0</version>
<scope>test</scope>
</dependency>
5.2 按类/包/标签选择测试(@SelectClasses, @IncludeTags 等)
JUnit Suite API 提供多种选择器注解,用于灵活组合测试用例。
| 选择器注解 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@SelectClasses | @SelectClasses({A.class, B.class}) | 显式指定要包含的测试类 | 见下方代码块 | 类必须是有效的 JUnit 测试类;可混合 JUnit 5 和 Vintage(JUnit 4)测试类。 |
@SelectPackages | @SelectPackages("com.example.service") | 包含指定包及其子包下的所有测试类 | 见下方代码块 | 递归扫描子包;适用于按模块组织测试的项目。 |
@SelectClassNamePatterns | @SelectClassNamePatterns({"*IntegrationTest", "*SmokeTest"}) | 通过正则表达式匹配类名 | @SelectClassNamePatterns("**/*IT.java") | 模式使用 Ant 风格通配符(* 匹配任意字符,** 匹配路径);注意转义特殊字符。 |
@IncludeTags / @ExcludeTags | @IncludeTags("fast") / @ExcludeTags("slow") | 基于 @Tag 标签过滤测试 | 见下方代码块 | 需配合测试类或方法上的 @Tag("database") 使用;@IncludeTags 是”或”关系(满足任一即包含),@ExcludeTags 优先级更高。 |
@Tag(测试侧) | @Tag("integration") | 为测试类或方法打标签 | 见下方代码块 | 标签名区分大小写;推荐使用小写+连字符(如 "db-integration");标签可叠加。 |
@IncludeEngines / @ExcludeEngines | @IncludeEngines("junit-jupiter") | 仅包含指定测试引擎的测试 | @IncludeEngines("junit-vintage") | 引擎 ID:junit-jupiter(JUnit 5)、junit-vintage(JUnit 3/4);适用于混合测试环境。 |
| 组合使用示例 | 多个选择器可同时使用 | 实现复杂筛选逻辑 | 见下方代码块 | 选择器之间是”与”关系:必须同时满足所有条件;例如,类必须在指定包中 且 带有 “critical” 标签 且 不带 “broken” 标签。 |
@SelectClasses / @SelectPackages 示例:
// 按类选择
@Suite
@SelectClasses({LoginTest.class, ProfileTest.class})
class AuthTestSuite {}
// 按包选择
@Suite
@SelectPackages("com.example.api")
class ApiTestSuite {}
@Tag 与 @IncludeTags / @ExcludeTags 示例:
// 测试侧 - 打标签
@Tag("security")
class SecurityTest {
@Test
@Tag("ldap")
void testLdapAuth() { ... }
}
// 套件侧 - 按标签筛选
@Suite
@IncludeTags("database")
@SelectPackages("com.example")
class DatabaseTestSuite {}
组合使用示例:
@Suite
@SelectPackages("com.example")
@IncludeTags("critical")
@ExcludeTags("broken")
class CriticalTestSuite {}
标签使用建议:
常见标签:fast、slow、integration、database、external、smoke、regression。构建脚本中可通过系统属性动态启用标签(如 Maven Surefire 的 groups/excludedGroups)。
第六章:扩展模型(Extensions)
6.1 扩展接口概览(BeforeEachCallback, AfterTestExecutionCallback 等)
JUnit 5 提供多个扩展点接口,允许在测试生命周期各阶段插入自定义逻辑。
| 扩展接口名称 | 触发时机 | 核心方法 | 用途 | 注意事项 |
|---|---|---|---|---|
BeforeAllCallback | 在 @BeforeAll 方法前执行(整个测试类开始前) | void beforeAll(ExtensionContext context) | 全局初始化(如启动嵌入式数据库) | 需配合 @ExtendWith 使用;若抛出异常,整个测试类被跳过。 |
AfterAllCallback | 在 @AfterAll 方法后执行(整个测试类结束后) | void afterAll(ExtensionContext context) | 全局清理(如关闭服务器) | 即使测试失败也会执行;注意资源释放顺序。 |
BeforeEachCallback | 在 @BeforeEach 方法前执行(每个测试方法前) | void beforeEach(ExtensionContext context) | 方法级初始化(如重置状态) | 执行顺序:BeforeAll → BeforeEach → Test;可访问当前测试方法信息。 |
AfterEachCallback | 在 @AfterEach 方法后执行(每个测试方法后) | void afterEach(ExtensionContext context) | 方法级清理(如回滚事务) | 执行顺序:Test → AfterEach → AfterEachCallback;即使测试失败也会执行。 |
BeforeTestExecutionCallback | 在测试方法体执行前(@BeforeEach 之后) | void beforeTestExecution(ExtensionContext context) | 性能监控、日志记录等 | 位于实际测试逻辑之前,适合 AOP 式增强。 |
AfterTestExecutionCallback | 在测试方法体执行后(无论成功/失败) | void afterTestExecution(ExtensionContext context) | 捕获测试结果、生成报告 | 可通过 context.getExecutionException() 判断是否失败。 |
TestExecutionExceptionHandler | 当测试方法抛出异常时 | void handleTestExecutionException(ExtensionContext context, Throwable throwable) | 自定义异常处理(如重试、转换) | 若处理后仍抛出异常,测试标记为失败;可吞掉异常使测试”通过”(不推荐)。 |
ParameterResolver | 为测试方法参数提供值 | boolean supportsParameter(...) / Object resolveParameter(...) | 实现依赖注入(如自动注入 Mock 对象) | 需与 @ExtendWith 配合;常用于替代构造器/字段注入。 |
TestWatcher | 监听测试生命周期事件 | testSuccessful / testFailed / testAborted 等 | 记录测试结果、生成审计日志 | 不影响测试流程;适合非侵入式监控。 |
ExtensionContext 关键上下文对象,可获取:
- 当前测试类/方法(
getRequiredTestClass(),getTestMethod()) - 测试显示名称(
getDisplayName()) - 存储临时数据(
getStore()) - 获取注解(
getElement().getAnnotation(...))
6.2 自定义扩展实现
通过实现上述接口并使用 @ExtendWith 注册,可创建可复用的扩展逻辑。
| 操作步骤 | 操作细节 | 注意事项 |
|---|---|---|
| 定义扩展类 | 创建类实现一个或多个扩展接口 | 推荐单一职责(如只实现 BeforeEachCallback);类可为 static 或 top-level |
| 注册扩展(类级别) | 在测试类上标注 @ExtendWith(MyExtension.class) | 扩展对类中所有测试方法生效;可叠加多个 @ExtendWith({A.class, B.class}) |
| 注册扩展(方法级别) | 在测试方法上标注 @ExtendWith(MyExtension.class) | 仅对该方法生效;优先级高于类级别 |
使用 ExtensionContext | 通过 context 获取测试元数据或存储状态 | 调用 getStore(Namespace) 获取隔离存储空间,避免跨测试污染 |
计时扩展示例:
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final Namespace NAMESPACE = Namespace.create("timing");
public void beforeTestExecution(ExtensionContext context) {
long start = System.currentTimeMillis();
context.getStore(NAMESPACE).put("start", start);
}
public void afterTestExecution(ExtensionContext context) {
long start = context.getStore(NAMESPACE).get("start", long.class);
long duration = System.currentTimeMillis() - start;
System.out.println(context.getDisplayName() + " 耗时: " + duration + "ms");
}
}
注意:必须使用
Namespace隔离不同测试的数据;System.out仅作演示,生产环境应使用日志框架。
| 方法/注解名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@ExtendWith | @ExtendWith(TimingExtension.class) | 注册自定义扩展 | @ExtendWith(TimingExtension.class) class MyTest { @Test void testX() { ... } } | 可重复注解(Java 8+);也可通过 META-INF/services 机制全局注册(高级用法) |
Namespace.create | Namespace.create("my-extension") | 创建唯一命名空间 | private static final Namespace NS = Namespace.create("cache"); | 避免与其他扩展冲突;建议使用包名作为前缀 |
6.3 内置扩展(如 MockitoExtension)
JUnit 5 社区和官方提供多个常用内置扩展,简化常见测试场景。
| 内置扩展名称 | 所属库 | 用途 | 使用方式 | 注意事项 |
|---|---|---|---|---|
MockitoExtension | org.mockito:mockito-junit-jupiter | 自动初始化 @Mock、@InjectMocks 字段 | 见下方代码块 | 需添加依赖;字段必须是非 private;支持 @Spy、@Captor 等 |
SpringExtension | org.springframework:spring-test | 集成 Spring Boot 测试(@SpringBootTest) | @ExtendWith(SpringExtension.class) + @SpringBootTest | 通常通过 @SpringBootTest 间接启用,无需显式写 @ExtendWith |
TemporaryDirectory | JUnit Jupiter 内置 | 自动创建并清理临时目录 | @Test void test(@TempDir Path tempDir) { ... } | @TempDir 可用于参数(ParameterResolver)或字段;目录在测试后自动删除 |
RepeatedTest | JUnit Jupiter 内置(非传统扩展) | 重复执行测试 | @RepeatedTest(3) void testFlakyFeature() { ... } | 虽非 @ExtendWith 形式,但基于扩展模型实现;每次重复视为独立测试 |
DisabledCondition | JUnit Jupiter 内置 | 支撑 @Disabled、@EnabledOnOs 等条件注解 | 无需手动注册 | 由 JUnit 自动加载;开发者通常只使用高层注解 |
MockitoExtension 依赖配置:
<!-- Maven -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
MockitoExtension 使用示例:
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock Database db;
@InjectMocks UserService service;
@Test void testLogin() { ... }
}
TemporaryDirectory 使用示例:
@Test
void testFileWrite(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test.txt");
Files.write(file, "hello".getBytes());
assertTrue(Files.exists(file));
}
最佳实践:
- 优先使用成熟扩展(如
MockitoExtension),避免重复造轮子- 自定义扩展应保持无状态或使用
Namespace隔离状态- 扩展逻辑应轻量,避免显著拖慢测试速度
第七章:高级特性与集成
7.1 动态测试(@TestFactory)
@TestFactory 允许在运行时动态生成测试用例,适用于数据驱动但参数无法静态确定的场景。
| 注解/方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
@TestFactory | 标注返回 Stream、Collection 等的方法 | 动态生成多个独立测试实例 | 见下方代码块 | 方法不能是 void;必须返回支持的容器类型(Stream / Collection / Iterable / Iterator);每个 DynamicTest 是独立测试,失败互不影响。 |
DynamicTest.dynamicTest | static DynamicTest dynamicTest(String displayName, Executable executable) | 构造一个动态测试实例 | dynamicTest("检查正数", () -> assertTrue(value > 0)); | displayName 支持任意字符串;executable 是无参无返回 Lambda;不支持生命周期回调(如 @BeforeEach 不对其生效)。 |
| 返回类型支持 | Stream, Collection, Iterable, Iterator | 提供灵活的数据源 | 见下方代码块 | 推荐使用 Stream(惰性求值)避免预加载大量数据;若数据量小,List 更直观。 |
与 @ParameterizedTest 区别 | @ParameterizedTest:编译时已知参数 / @TestFactory:运行时生成测试 | 选择依据:参数是否可静态枚举 | —— | 若参数来自文件、数据库、网络等运行时源,用 @TestFactory;否则优先用 @ParameterizedTest(支持生命周期、标签等)。 |
动态测试示例:
import org.junit.jupiter.api.DynamicTest;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
@TestFactory
Stream<DynamicTest> testFileParsing() {
return Files.list(Paths.get("test-cases"))
.map(path -> dynamicTest(
"解析文件: " + path.getFileName(),
() -> assertValid(parse(path))
));
}
使用 List 返回示例:
@TestFactory
List<DynamicTest> testCases() {
List<DynamicTest> tests = new ArrayList<>();
for (String case : cases) {
tests.add(dynamicTest(case, () -> runTest(case)));
}
return tests;
}
7.2 与 Maven / Gradle 集成
正确配置构建工具是批量执行 JUnit 5 测试的前提。
Maven 集成
| 配置项 | 操作细节 | 注意事项 |
|---|---|---|
| 添加依赖 | 必须包含 junit-jupiter(含 API + params + engine) | —— |
| 配置 Surefire 插件 | Surefire ≥ 2.22.0 才原生支持 JUnit Platform;旧版本需额外配置 provider | —— |
| 执行命令 | mvn test | 自动发现并运行所有符合命名规则的测试类(默认 **/*Test.java) |
| 指定测试类 | mvn test -Dtest=UserServiceTest | 支持通配符:mvn test -Dtest=*ServiceTest |
| 跳过测试 | mvn install -DskipTests | 或 -Dmaven.test.skip=true(同时跳过编译测试代码) |
Maven 完整配置示例:
<!-- 依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Surefire 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M9</version>
</plugin>
Gradle 集成
| 配置项 | 操作细节 | 注意事项 |
|---|---|---|
| 应用 Java 插件 | plugins { id 'java' } | 必须启用才能识别 src/test/java |
| 添加依赖 | testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' | 不需要单独引入 engine,junit-jupiter 已包含 |
| 启用 JUnit Platform | test { useJUnitPlatform() } | 关键步骤! 若省略,Gradle 默认使用 JUnit 4,导致测试不执行 |
| 执行命令 | ./gradlew test | 生成 HTML 报告于 build/reports/tests/test/index.html |
| 过滤测试 | ./gradlew test --tests "*ServiceTest" | 支持类名或方法名匹配 |
| 跳过测试 | ./gradlew build -x test | -x 表示 exclude task |
Gradle 完整配置示例:
plugins {
id 'java'
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
test {
useJUnitPlatform()
}
7.3 与 IDE(IntelliJ IDEA / VS Code)集成
现代 IDE 原生支持 JUnit 5,提供一键运行、调试和结果可视化。
| IDE | 配置要求 | 使用方式 | 注意事项 |
|---|---|---|---|
| IntelliJ IDEA | 2019.2 或更高版本 | 右键测试类/方法 → Run;点击编辑器左侧绿色 ▶️ | 无需额外插件;自动识别 JUnit 5;若未运行,检查 Project SDK 是否 ≥ Java 8 |
| VS Code | 安装 Extension Pack for Java(含 Language Support for Java + Debugger) | 在测试方法上出现 “Run Test” / “Debug Test” CodeLens | 需确保项目根目录有 pom.xml 或 build.gradle;首次运行可能需下载依赖 |
| Eclipse | 2018-12 或更高版本 | 右键 → Run As → JUnit Test | 若提示 “No JUnit tests found”,检查是否启用了 JUnit 5 支持(Project Properties → Java Build Path → Libraries) |
通用问题排查:
- 依赖是否完整
- JDK 版本是否 ≥ 8
- 是否误用 JUnit 4 注解
- 常见错误:
ClassNotFoundException(缺依赖)、TestEngine not found(缺junit-platform-engine) - 查看 IDE 控制台错误信息定位问题
7.4 生成测试报告(Surefire / Jacoco)
通过构建工具生成结构化测试报告和代码覆盖率分析。
Surefire 测试报告(Maven)
| 报告类型 | 生成位置 | 内容 | 配置方式 |
|---|---|---|---|
| XML 报告 | target/surefire-reports/TEST-*.xml | 供 CI 系统(如 Jenkins)解析 | 默认生成,无需额外配置 |
| HTML 报告 | target/site/surefire-report.html | 人类可读的测试摘要 | 需运行 mvn surefire-report:report |
| 自定义输出目录 | target/custom-reports | —— | 适用于多模块项目隔离报告 |
自定义输出目录配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<reportsDirectory>${project.build.directory}/custom-reports</reportsDirectory>
</configuration>
</plugin>
JaCoCo 代码覆盖率
| 工具 | 配置方式 | 报告位置 | 注意事项 |
|---|---|---|---|
| Maven + JaCoCo | org.jacoco:jacoco-maven-plugin:0.8.11 | target/site/jacoco/index.html | prepare-agent 必须在 test 前执行;report 生成 HTML 覆盖率报告 |
| Gradle + JaCoCo | plugins { id 'jacoco' } | build/reports/jacoco/test/html/index.html | finalizedBy 确保测试后自动生成报告 |
Maven JaCoCo 配置:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
Gradle JaCoCo 配置:
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.11"
}
test {
finalizedBy jacocoTestReport
}
jacocoTestReport {
reports {
html.required = true
xml.required = false
}
}
覆盖率指标:
行覆盖(Line)、分支覆盖(Branch)、方法覆盖(Method)。报告中高亮未覆盖代码。可设置覆盖率阈值(如 BUNDLE_LINE_COVERED_RATIO = 0.8),低于则构建失败。
最佳实践:
- CI 流程中应包含
mvn test+mvn jacoco:report- 覆盖率目标建议:核心模块 ≥ 80%,非核心 ≥ 60%
- 避免为追求覆盖率而写无意义断言
第八章:最佳实践与常见问题
8.1 测试命名规范
良好的命名提升可读性、可维护性和故障定位效率。
| 命名维度 | 推荐规范 | 示例 | 注意事项 |
|---|---|---|---|
| 测试类名 | 被测类名 + Test 或场景 + Test | UserServiceTest、LoginSecurityTest | 避免泛化名称如 Test1;若测试特定行为,可加后缀如 UserServiceIntegrationTest |
| 测试方法名 | 行为驱动命名:should[期望行为]When[触发条件] 或 [给定]_[当]_[则](Given_When_Then) | shouldReturnUserWhenIdExists()、givenValidToken_whenAccessResource_thenGrantAccess() | 避免 testXxx()(JUnit 3 风格);方法名应完整表达业务意图 |
显示名称(@DisplayName) | 使用自然语言描述测试目的 | @DisplayName("用户登录失败时应返回 401 状态码") | 支持中文、Emoji(如 ”✅ 正常流程” / “⚠️ 异常输入”);仅用于报告展示,不影响执行 |
| 参数化测试命名模板 | 使用 {index}, {argumentsWithNames} 提高可读性 | @ParameterizedTest(name = "输入 {0} 应抛出 IllegalArgumentException") | 默认模板较晦涩,建议显式定义 name 属性 |
命名反模式:
test1,testMethod→ 无意义checkIfUserIsValid→ 未说明”在什么条件下”- 全大写或下划线分隔(如
TEST_USER_LOGIN)→ 不符合 Java 方法命名惯例
8.2 测试隔离与可重复性
每个测试应独立运行,不受其他测试影响,且结果可稳定复现。
| 原则 | 实现方式 | 示例 | 注意事项 |
|---|---|---|---|
| 状态隔离 | 每个测试使用独立实例或重置共享状态 | @BeforeEach void setUp() { userService = new UserService(); } | 避免在测试类中使用非 final 静态字段存储状态(如 static List users) |
| 外部依赖隔离 | 使用 Mock(Mockito)、内存数据库(H2)、临时文件(@TempDir) | @Mock Database db; / @TempDir Path tempFolder; | 真实网络/数据库调用会导致慢、不稳定、不可控 |
| 时间隔离 | 注入 Clock 或固定时间源 | private Clock clock = Clock.fixed(Instant.parse("2023-01-01T00:00:00Z"), ZoneId.of("UTC")); | 避免直接调用 System.currentTimeMillis() 或 LocalDate.now() |
| 随机性控制 | 固定随机种子 | Random random = new Random(12345L); | 若必须用随机数据,应在失败时记录种子以便复现 |
| 资源清理 | 使用 @AfterEach / @AfterAll 或 try-with-resources | @AfterEach void tearDown() { if (server != null) server.stop(); } | 即使测试失败也应释放资源(如文件句柄、端口) |
| 并发安全 | 默认 JUnit 5 测试不并发执行;若启用并行,确保线程安全 | —— | 并行测试需显式配置(如 junit-platform.properties 中 junit.jupiter.execution.parallel.enabled=true),此时隔离要求更高 |
破坏隔离的典型行为:
- 在测试 A 中修改静态变量,测试 B 依赖该变量
- 多个测试共用同一个数据库连接且未回滚事务
- 测试方法之间存在执行顺序依赖(如必须先 run
testCreate再 runtestDelete)
8.3 常见错误排查(如静态方法误用、生命周期混淆等)
以下是 JUnit 5 开发中最常见的错误及其解决方案。
| 错误现象 | 根本原因 | 排查方法 | 解决方案 |
|---|---|---|---|
@Test 方法未执行 | 方法为 private / static / 有返回值 | 查看 IDE 是否显示绿色 ▶️;检查控制台是否提示 “No tests found” | 确保方法为 public void methodName()(或 package-private),无参数,无返回值 |
@BeforeAll 报错 “static method required” | @BeforeAll 方法未声明为 static | 编译或运行时报错:@BeforeAll method 'init' must be static | 将方法改为 static;或添加 @TestInstance(Lifecycle.PER_CLASS) 并保留非静态 |
| 断言失败但异常信息模糊 | 未提供自定义失败消息 | AssertionError 仅显示 expected vs actual | 使用带 message 的断言:assertEquals(expected, actual, "用户ID应匹配") 或 Lambda 延迟求值:assertTrue(condition, () -> "当前值: " + value) |
| 参数化测试参数数量/类型不匹配 | @CsvSource 行数 ≠ 方法参数数 | 运行时报 ParameterResolutionException | 检查每行 CSV 参数个数;确认类型可自动转换(如 String → int)或提供 ArgumentConverter |
| 扩展未生效 | 忘记添加 @ExtendWith 或依赖缺失 | 自定义逻辑未执行;Mockito 字段为 null | 检查是否标注 @ExtendWith(MyExtension.class);确认 Maven/Gradle 已引入相关库(如 mockito-junit-jupiter) |
测试套件(@Suite)不运行 | 缺少 junit-platform-suite 依赖 | 运行套件类时报 ClassNotFoundException | 添加依赖:Maven: org.junit.platform:junit-platform-suite / Gradle: testImplementation 'org.junit.platform:junit-platform-suite' |
动态测试(@TestFactory)不显示为多个测试 | 返回类型错误(如 void) | IDE 仅显示一个测试项 | 确保返回 Stream<DynamicTest> 等支持类型;不要用 void 方法 |
| JaCoCo 覆盖率为 0% | 未正确配置 agent | HTML 报告中所有类灰色 | Maven:确保 jacoco 插件包含 <goal>prepare-agent</goal> / Gradle:确认应用了 id 'jacoco' 且 useJUnitPlatform() 已启用 |
通用排查技巧:
- 最小可复现示例:新建简单测试类验证问题是否仍存在
- 查看完整堆栈:IDE 控制台或
surefire-reports/中的.txt文件 - 检查依赖树:
mvn dependency:tree或./gradlew dependencies,确认无版本冲突 - 升级到最新版:JUnit 5.10+ 修复了大量历史问题
附录 A:JUnit 5 常用注解速查表
| 注解名称 | 所在包 | 作用位置 | 功能说明 | 典型用法示例 |
|---|---|---|---|---|
@Test | org.junit.jupiter.api | 方法 | 标识一个标准测试方法 | @Test void testAdd() { ... } |
@ParameterizedTest | org.junit.jupiter.params | 方法 | 标识参数化测试方法 | @ParameterizedTest @ValueSource(ints={1,2}) void testX(int n) { ... } |
@RepeatedTest | org.junit.jupiter.api | 方法 | 重复执行测试 N 次 | @RepeatedTest(3) void testFlaky() { ... } |
@TestFactory | org.junit.jupiter.api | 方法 | 动态生成测试用例 | @TestFactory Stream<DynamicTest> tests() { ... } |
@BeforeEach | org.junit.jupiter.api | 方法 | 每个测试方法前执行 | @BeforeEach void setUp() { ... } |
@AfterEach | org.junit.jupiter.api | 方法 | 每个测试方法后执行 | @AfterEach void tearDown() { ... } |
@BeforeAll | org.junit.jupiter.api | 方法 | 整个测试类开始前执行(需 static 或 PER_CLASS) | @BeforeAll static void init() { ... } |
@AfterAll | org.junit.jupiter.api | 方法 | 整个测试类结束后执行 | @AfterAll static void cleanup() { ... } |
@DisplayName | org.junit.jupiter.api | 类 / 方法 | 自定义测试显示名称(支持中文/Emoji) | @DisplayName("✅ 用户登录成功") |
@Disabled | org.junit.jupiter.api | 类 / 方法 | 禁用测试(跳过执行) | @Disabled("待修复") @Test void brokenTest() { ... } |
@Tag | org.junit.jupiter.api | 类 / 方法 | 为测试打标签,用于分组筛选 | @Tag("integration") class IT { ... } |
@ExtendWith | org.junit.jupiter.api | 类 / 方法 | 注册自定义或内置扩展 | @ExtendWith(MockitoExtension.class) |
@TempDir | org.junit.jupiter.api | 参数 / 字段 | 注入临时目录 Path(自动清理) | @Test void test(@TempDir Path tmp) { ... } |
@Suite | org.junit.platform.suite.api | 类 | 定义测试套件入口 | @Suite @SelectClasses({A.class}) class MySuite {} |
@SelectClasses | org.junit.platform.suite.api | 类(配合 @Suite) | 指定套件包含的测试类 | @SelectClasses({LoginTest.class}) |
@IncludeTags / @ExcludeTags | org.junit.platform.suite.api | 类(配合 @Suite) | 按标签包含/排除测试 | @IncludeTags("fast") |
@ValueSource / @CsvSource / @MethodSource | org.junit.jupiter.params | 方法(配合 @ParameterizedTest) | 提供参数化测试数据源 | @ValueSource(strings={"a","b"}) |
生命周期顺序(单个测试方法):
@BeforeAll → @BeforeEach → @Test / @ParameterizedTest → @AfterEach → @AfterAll
附录 B:JUnit 5 断言方法汇总表(org.junit.jupiter.api.Assertions)
| 断言方法 | 签名(常用重载) | 用途 | 示例 |
|---|---|---|---|
assertEquals | assertEquals(expected, actual) / assertEquals(expected, actual, delta) / assertEquals(expected, actual, String message) | 断言两个值相等(使用 equals) | assertEquals(4, 2+2); / assertEquals(0.1, result, 0.001); |
assertNotEquals | assertNotEquals(unexpected, actual) | 断言两个值不相等 | assertNotEquals(null, obj); |
assertTrue | assertTrue(boolean condition) / assertTrue(condition, Supplier<String> message) | 断言条件为 true | assertTrue(list.isEmpty(), () -> "size=" + list.size()); |
assertFalse | assertFalse(boolean condition) | 断言条件为 false | assertFalse(str.isBlank()); |
assertNull | assertNull(Object actual) | 断言对象为 null | assertNull(cache.get("key")); |
assertNotNull | assertNotNull(Object actual) | 断言对象非 null | assertNotNull(result); |
assertSame | assertSame(expected, actual) | 断言两个引用指向同一对象(==) | assertSame(instance, getInstance()); |
assertNotSame | assertNotSame(unexpected, actual) | 断言两个引用不指向同一对象 | assertNotSame(new Object(), obj); |
assertThrows | assertThrows(Class<T> type, Executable executable) | 断言执行时抛出指定异常 | IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> divide(1,0)); |
assertDoesNotThrow | assertDoesNotThrow(Executable executable) | 断言执行时不抛出异常 | String s = assertDoesNotThrow(() -> parse(input)); |
assertTimeout | assertTimeout(Duration timeout, Executable executable) | 断言在超时内完成(当前线程) | assertTimeout(Duration.ofSeconds(1), () -> compute()); |
assertTimeoutPreemptively | assertTimeoutPreemptively(Duration timeout, Executable executable) | 断言在超时内完成(独立线程,可中断) | assertTimeoutPreemptively(Duration.ofMillis(100), () -> loopForever()); |
assertAll | assertAll(Stream<Executable> executables) / assertAll(String heading, Executable... execs) | 组合多个断言,统一报告失败 | assertAll("user", () -> assertEquals("Tom", u.name), () -> assertTrue(u.active)); |
fail | fail(String message) / fail(Throwable cause) | 主动使测试失败 | if (unsupported) fail("不支持该配置"); |
关键说明:
- 所有断言方法均位于
org.junit.jupiter.api.Assertions,通常静态导入:import static org.junit.jupiter.api.Assertions.*; Executable是函数式接口:() -> { /* code */ }Supplier<String>用于延迟构建错误消息,避免无谓字符串拼接开销- 浮点数比较必须使用带
delta的assertEquals,避免精度问题