1. 项目概述为什么我们需要JUnit 5如果你是一个Java开发者并且你的项目里还没有单元测试或者你的测试代码还停留在JUnit 4甚至更早的版本那么你可能正在错过提升代码质量和开发效率的最佳工具。单元测试早已不是“锦上添花”的选项而是现代软件工程中保障代码健壮性、支持持续集成和重构的基石。JUnit作为Java生态中最主流的单元测试框架其最新的JUnit 5版本不仅仅是一次简单的版本迭代而是一次从架构到理念的全面革新。我见过太多项目测试代码写得像面条一样混乱一个测试方法里塞满了各种断言和逻辑维护起来比业务代码还头疼。也见过团队因为测试依赖复杂、运行缓慢最终放弃了编写测试。JUnit 5正是为了解决这些痛点而生的。它引入了模块化架构、更强大的断言机制、灵活的测试生命周期管理以及对Java 8及以上版本特性的原生支持比如Lambda表达式。简单来说JUnit 5让你写测试变得更简单、更优雅、也更强大。这篇指南不会只停留在“Hello World”式的示例我会结合我多年在一线项目中踩过的坑和总结的最佳实践带你从“会用”进阶到“精通”真正把单元测试变成你开发流程中得心应手的利器。2. JUnit 5 核心架构与模块化设计JUnit 5的架构设计是其与旧版本最根本的区别。它被分成了三个明确分离的子项目这种模块化设计带来了极大的灵活性和可扩展性。2.1 三大模块详解JUnit Platform这是测试执行的基础。它定义了一个稳定、强大的API用于在JVM上启动测试框架。你的IDE如IntelliJ IDEA、Eclipse、构建工具如Maven、Gradle甚至持续集成服务器都是通过实现JUnit Platform提供的TestEngineAPI来发现和执行测试的。你可以把它想象成电脑的主板和电源为所有测试引擎提供运行环境。JUnit Jupiter这是编写新测试的核心编程模型和扩展模型。它包含了全新的注解如TestBeforeEach、断言库Assertions和扩展模型Extension。我们日常编写测试代码时接触最多的就是这个模块。它就像是安装在主板上的CPU和内存负责具体的计算测试任务。JUnit Vintage这是一个为了向后兼容而存在的模块。它提供了一个TestEngine用于在JUnit 5平台上运行基于JUnit 3或JUnit 4编写的旧测试。这确保了项目可以平滑迁移不必一次性重写所有历史测试用例。它相当于一个老式的PCI插槽让你还能用上那些经典的“老爷卡”旧测试。注意在Maven项目中你通常只需要依赖junit-jupiter它本身会传递依赖junit-platform-commons等构建工具和IDE已经内置了对Platform的支持。只有当你需要运行JUnit 4测试时才需要显式引入junit-vintage-engine。2.2 依赖配置实战Maven/Gradle理解架构后配置依赖就清晰了。以下是最佳实践的依赖配置。Maven配置 在pom.xml中推荐使用junit-jupiter聚合依赖它帮你管理好了所有必要组件的版本。dependencies dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.10.0/version !-- 请使用最新稳定版 -- scopetest/scope /dependency /dependencies build plugins plugin artifactIdmaven-surefire-plugin/artifactId version3.0.0-M7/version /plugin /plugins /buildGradle配置 在build.gradle中配置更加简洁。dependencies { testImplementation org.junit.jupiter:junit-jupiter:5.10.0 } test { useJUnitPlatform() }为什么这么配使用聚合依赖junit-jupiter避免了手动管理多个子模块版本可能带来的不一致问题。确保maven-surefire-plugin版本在2.22.0以上以完整支持JUnit 5。在Gradle中useJUnitPlatform()这句声明至关重要它告诉Gradle使用JUnit Platform来执行测试。3. 编写你的第一个JUnit 5测试让我们从一个简单的例子开始感受JUnit 5的语法。假设我们有一个计算器类Calculator。public class Calculator { public int add(int a, int b) { return a b; } public int divide(int a, int b) { if (b 0) { throw new IllegalArgumentException(Divisor cannot be zero); } return a / b; } }对应的JUnit 5测试类如下import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { Test void testAddition() { Calculator calculator new Calculator(); int result calculator.add(2, 3); assertEquals(5, result, 2 3 should equal 5); } Test void testDivision() { Calculator calculator new Calculator(); int result calculator.divide(10, 2); assertEquals(5, result); } Test void testDivisionByZero() { Calculator calculator new Calculator(); Exception exception assertThrows(IllegalArgumentException.class, () - { calculator.divide(10, 0); }); assertEquals(Divisor cannot be zero, exception.getMessage()); } }核心变化与优势Test注解来自org.junit.jupiter.api包不再需要public修饰符。测试方法可以是包私有的这更符合封装思想。断言静态导入assertEqualsassertThrows等方法现在位于org.junit.jupiter.api.Assertions类中。使用静态导入能让代码更简洁。Lambda支持assertThrows的第二个参数是一个Executable函数式接口用Lambda表达式书写异常抛出的代码块非常直观。可选的失败信息assertEquals的最后一个参数是可选的消息它只在断言失败时才会被构造和输出。这避免了不必要的字符串拼接开销是性能上的一个小优化。4. 深入断言机制从基础到高阶强大的断言是有效测试的保障。JUnit Jupiter的断言库经过了重新设计功能全面且支持Lambda。4.1 基础断言除了常见的assertEqualsassertNotEqualsassertTrueassertFalseassertNullassertNotNull JUnit 5还提供了assertSame/assertNotSame断言两个对象引用是否指向同一个实例。assertArrayEquals断言数组内容相等。assertIterableEquals断言可迭代对象如List Set的内容相等。assertLinesMatch非常强大的字符串行匹配断言支持正则表达式和等特殊语法常用于匹配多行文本输出比如日志或命令行结果。4.2 组合断言与按顺序断言这是JUnit 5的一大亮点解决了“一个测试方法中断言失败后后续断言不执行”的问题让你能一次性看到所有失败点。assertAll分组断言确保组内的所有断言都被执行并汇总所有失败信息。Test void testPerson() { Person person new Person(John, Doe); assertAll(person, () - assertEquals(John, person.getFirstName()), () - assertEquals(Doe, person.getLastName()), () - assertTrue(person.getAge() 0, Age should be positive) ); }assertTimeout与assertTimeoutPreemptively超时断言用于验证代码执行时间。assertTimeout在同一个线程中执行即使超时也会等任务完成。assertTimeoutPreemptively在另一个线程中执行超时后会立即中断任务。谨慎使用因为中断线程可能导致资源未清理。Test void testTimeout() { // 任务必须在1秒内完成 assertTimeout(Duration.ofSeconds(1), () - { // 模拟耗时操作 Thread.sleep(500); }); }4.3 第三方断言库集成虽然JUnit自带的断言已经很强但有时你可能需要更富表达力的断言。AssertJ和Hamcrest是两个流行的选择。JUnit 5能很好地与它们集成。使用AssertJ示例 AssertJ提供了流式API断言读起来像自然语言。import static org.assertj.core.api.Assertions.*; Test void testWithAssertJ() { ListString list Arrays.asList(apple, banana, cherry); assertThat(list) .hasSize(3) .contains(banana, apple) .doesNotContain(grape) .allMatch(s - s.length() 3); }实操心得在大型项目中我倾向于使用AssertJ来处理复杂的对象图断言和集合断言它的错误信息通常更清晰。而对于简单的相等、布尔判断直接用JUnit的断言就足够了。混合使用可以兼顾效率和表达力。5. 测试生命周期与Fixture管理JUnit 5提供了清晰且灵活的生命周期钩子用于管理测试前后的资源。5.1 生命周期注解BeforeAll/AfterAll在整个测试类的所有测试方法之前和之后各执行一次。方法必须是static的。常用于初始化昂贵资源如数据库连接池、嵌入式服务器。BeforeEach/AfterEach在每个测试方法之前和之后各执行一次。方法不需要是static的。常用于重置测试状态如清理数据库表、重置模拟对象。TestInstance这是一个类级别的注解用于配置测试实例的生命周期。默认是Lifecycle.PER_METHOD每个测试方法创建一个新的测试类实例。可以设置为Lifecycle.PER_CLASS整个测试类共享一个实例这样BeforeAll和AfterAll方法就可以不是static的了但需要小心处理测试间的状态污染。TestInstance(TestInstance.Lifecycle.PER_CLASS) class SharedResourceTest { private ExpensiveResource resource; BeforeAll void initAll() { // 现在可以不是static了 resource new ExpensiveResource(); resource.start(); } Test void testOne() { ... } Test void testTwo() { ... } AfterAll void tearDownAll() { resource.close(); } }5.2 资源自动管理TempDirJUnit 5.4引入了TempDir注解用于在测试中自动创建和清理临时文件或目录。这是处理文件I/O测试的神器再也不用担心测试后遗留垃圾文件了。Test void testWritingToTempFile(TempDir Path tempDir) throws IOException { Path testFile tempDir.resolve(test.txt); Files.writeString(testFile, Hello, JUnit 5!); ListString lines Files.readAllLines(testFile); assertEquals(List.of(Hello, JUnit 5!), lines); } // 测试结束后tempDir及其内容会被自动删除6. 参数化测试数据驱动测试的利器当你想用多组不同的输入数据来测试同一个逻辑时参数化测试能极大减少样板代码。JUnit 5的参数化测试功能非常强大。6.1 使用ValueSource提供一组字面量值基本类型、String、Class。ParameterizedTest ValueSource(ints {1, 3, 5, -3, 15}) void testIsOdd(int number) { assertTrue(MathUtils.isOdd(number)); }6.2 使用CsvSource和CsvFileSourceCsvSource直接在注解中写CSV格式的数据。CsvFileSource从类路径下的CSV文件加载数据。ParameterizedTest(name {0} {1} {2}) // 自定义测试显示名称 CsvSource({ 0, 1, 1, 1, 2, 3, 10, -2, 8 }) void testAddition(int a, int b, int expectedSum) { Calculator calc new Calculator(); assertEquals(expectedSum, calc.add(a, b)); } ParameterizedTest CsvFileSource(resources /test-data.csv, numLinesToSkip 1) void testWithCsvFile(String name, int age) { // 从src/test/resources/test-data.csv读取数据 }6.3 使用MethodSource引用一个返回StreamIterableIterator或数组的静态工厂方法适合提供复杂的对象作为参数。ParameterizedTest MethodSource(provideStringsForTest) void testStringLength(String input, int expectedLength) { assertEquals(expectedLength, input.length()); } private static StreamArguments provideStringsForTest() { return Stream.of( Arguments.of(hello, 5), Arguments.of(JUnit, 5), Arguments.of(, 0) ); }6.4 自定义参数提供器 (ArgumentsSource)对于更复杂的参数化需求你可以实现ArgumentsProvider接口。class CustomArgumentsProvider implements ArgumentsProvider { Override public Stream? extends Arguments provideArguments(ExtensionContext context) { return Stream.of( Arguments.of(new ComplexObject(A, 1)), Arguments.of(new ComplexObject(B, 2)) ); } } ParameterizedTest ArgumentsSource(CustomArgumentsProvider.class) void testWithCustomProvider(ComplexObject obj) { // ... }注意事项参数化测试的方法本身需要用ParameterizedTest注解而不是普通的Test。合理利用name属性可以让测试报告更具可读性。当数据量很大时优先考虑CsvFileSource或MethodSource保持代码整洁。7. 动态测试运行时生成测试用例与参数化测试在编译时确定数据不同动态测试允许你在运行时动态生成测试用例。这对于测试那些用例无法在编码时完全确定或者需要从外部系统数据库、文件、网络动态加载的场景非常有用。动态测试通过TestFactory注解来标识一个工厂方法该方法返回DynamicTest或StreamCollectionIterableIterator的DynamicTest。import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.DynamicTest.dynamicTest; class DynamicTestsExample { TestFactory StreamDynamicTest dynamicTestsFromStream() { ListString inputList Arrays.asList(apple, banana, cherry); return inputList.stream() .map(input - dynamicTest(Testing length of: input, () - assertTrue(input.length() 0))); } TestFactory ListDynamicTest dynamicTestsFromList() { return List.of( dynamicTest(1st dynamic test, () - assertTrue(true)), dynamicTest(2nd dynamic test, () - assertEquals(4, 2 * 2)) ); } }动态测试 vs 参数化测试参数化测试适合已知的、有限的数据集所有用例共享相同的测试逻辑和生命周期每个用例都执行BeforeEach等。动态测试适合动态生成的、可能无限的测试用例集合每个DynamicTest是完全独立的执行单元不共享生命周期方法。实操心得动态测试的一个绝佳用途是“契约测试”或“兼容性测试”。例如你的系统需要支持一个插件生态你可以遍历plugins目录下的所有JAR文件为每个有效的插件动态生成一个测试用例验证其是否能被正确加载并实现某个接口。这在传统的静态测试中很难优雅地实现。8. 测试标签、过滤与嵌套随着测试套件规模的增长如何组织和管理测试变得至关重要。8.1 使用Tag进行分类你可以给测试类或方法打上标签然后根据标签来有选择地执行测试。Test Tag(fast) void fastTest() { // 快速运行的单元测试 } Test Tag(slow) Tag(integration) void integrationTest() { // 耗时的集成测试 }在Maven中通过标签过滤plugin artifactIdmaven-surefire-plugin/artifactId configuration groupsfast/groups !-- 只运行标记为fast的测试 -- !-- 或者排除某些标签 -- excludedGroupsslow, integration/excludedGroups /configuration /plugin在Gradle中通过标签过滤test { useJUnitPlatform { includeTags fast // excludeTags slow } }这样你可以在本地快速运行“fast”标签的单元测试而在CI/CD流水线中才运行所有的“slow”或“integration”测试。8.2 使用Nested构建层次结构Nested注解允许你在一个外部测试类中定义非静态的内部测试类从而在逻辑上对测试进行分组。嵌套类可以继承外部类的BeforeEach和AfterEach方法但BeforeAll和AfterAll在嵌套类中默认不工作除非将外部类改为TestInstance(Lifecycle.PER_CLASS)。class StackTests { StackObject stack; BeforeEach void createNewStack() { stack new Stack(); } Test void isEmpty() { assertTrue(stack.isEmpty()); } Nested class WhenEmpty { Test void popThrowsException() { assertThrows(EmptyStackException.class, () - stack.pop()); } } Nested class AfterPushingAnElement { String element an element; BeforeEach void pushAnElement() { stack.push(element); } Test void isNotEmpty() { assertFalse(stack.isEmpty()); } Test void popReturnsElement() { assertEquals(element, stack.pop()); } } }这种结构让测试报告更具可读性清晰地反映了被测试组件的状态和行为。9. 扩展模型JUnit 5的“超级武器”JUnit 5的扩展模型ExtensionAPI是其设计中最强大、最灵活的部分它取代了JUnit 4的Runner和Rule机制。通过实现BeforeEachCallbackAfterEachCallback等生命周期接口你可以以非侵入式的方式为测试添加自定义行为。9.1 内置扩展JUnit 5提供了一些开箱即用的扩展Disabled禁用测试。Timeout为单个测试方法设置超时。RepeatedTest重复运行测试指定次数。TempDir如前所述也是一个扩展。9.2 自定义扩展示例日志与计时假设我们想为每个测试方法自动记录开始、结束时间和耗时。import org.junit.jupiter.api.extension.*; public class TimingExtension implements BeforeEachCallback, AfterEachCallback { private static final Logger LOG LoggerFactory.getLogger(TimingExtension.class); private static final String START_TIME_KEY start-time; Override public void beforeEach(ExtensionContext context) { context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod())) .put(START_TIME_KEY, System.currentTimeMillis()); LOG.info(Starting test: {}, context.getDisplayName()); } Override public void afterEach(ExtensionContext context) { long startTime context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod())) .remove(START_TIME_KEY, long.class); long duration System.currentTimeMillis() - startTime; LOG.info(Finished test: {} in {} ms, context.getDisplayName(), duration); } }使用自定义扩展ExtendWith(TimingExtension.class) class MyServiceTest { // 你的测试方法... }9.3 条件测试执行通过实现ExecutionCondition接口可以基于环境条件动态地启用或禁用测试。这是实现“仅在Windows上运行”、“仅在数据库可用时运行”等条件测试的关键。public class RunOnWindowsCondition implements ExecutionCondition { Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { String os System.getProperty(os.name).toLowerCase(); if (os.contains(win)) { return ConditionEvaluationResult.enabled(Test enabled on Windows); } return ConditionEvaluationResult.disabled(Test disabled on non-Windows OS); } } // 使用自定义条件注解 Target({ ElementType.TYPE, ElementType.METHOD }) Retention(RetentionPolicy.RUNTIME) ExtendWith(RunOnWindowsCondition.class) public interface RunOnWindows {} RunOnWindows Test void windowsOnlyTest() { ... }扩展模型的威力在于其组合性。你可以将多个扩展日志、条件执行、事务管理、依赖注入等组合在一起为你的测试套件构建一个强大而定制化的运行环境。10. 与Mock框架集成Mockito单元测试的核心是“隔离”而Mock框架是创建测试替身Mock Stub Spy以实现隔离的最佳工具。Mockito是Java生态中最流行的Mock框架与JUnit 5集成非常顺畅。10.1 使用JUnit Jupiter扩展最优雅的方式是使用mockito-junit-jupiter依赖它提供了ExtendWith(MockitoExtension.class)。dependency groupIdorg.mockito/groupId artifactIdmockito-junit-jupiter/artifactId version5.7.0/version !-- 使用与Mockito核心一致的版本 -- scopetest/scope /dependencyimport org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.Mockito.*; ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock private PaymentGateway paymentGateway; // 被Mock的依赖 Mock private InventoryService inventoryService; InjectMocks private OrderService orderService; // 被测对象依赖会自动注入 Test void shouldProcessOrderSuccessfully() { // 1. 准备数据 (Given) Order order new Order(order-123, 100.0); when(paymentGateway.charge(anyDouble())).thenReturn(new PaymentResult(true, success)); when(inventoryService.reserveItem(anyString())).thenReturn(true); // 2. 执行操作 (When) boolean result orderService.process(order); // 3. 验证行为与状态 (Then) assertTrue(result); verify(paymentGateway).charge(100.0); verify(inventoryService).reserveItem(order-123); // 验证order状态是否已更新 assertEquals(OrderStatus.COMPLETED, order.getStatus()); } Test void shouldFailWhenPaymentFails() { Order order new Order(order-456, 50.0); when(paymentGateway.charge(anyDouble())).thenReturn(new PaymentResult(false, insufficient funds)); boolean result orderService.process(order); assertFalse(result); assertEquals(OrderStatus.FAILED, order.getStatus()); verify(inventoryService, never()).reserveItem(anyString()); // 验证库存服务从未被调用 } }10.2 关键点解析Mock创建一个Mock对象。其所有方法默认返回“空值”0 false null 空集合等。InjectMocks创建被测类的实例并自动将标记了Mock或Spy的字段注入进去通过构造函数、setter或字段注入。when(...).thenReturn(...)定义Mock对象的行为Stubbing。verify(mock).methodCall(...)验证Mock对象上的特定方法是否被调用以及调用的次数和参数。这是行为验证的关键。never()times(n)验证方法从未被调用或被调用了特定次数。BDD风格Mockito也支持BDD行为驱动开发风格的given(...).willReturn(...)和then(...).should(...)使测试读起来更像自然语言。避坑技巧一个常见的反模式是“过度Mock”。不要Mock你无法控制的东西如第三方库的核心类也不要Mock值对象如简单的POJO。Mock应该主要用于隔离那些有副作用如数据库、网络调用或行为复杂如随机数生成器的依赖。对于简单的数据转换或工具类直接使用真实对象或测试替身Test Double即可。11. 测试覆盖率与持续集成写了测试如何衡量其有效性测试覆盖率是一个重要的量化指标尽管不是唯一指标。JaCoCo是Java生态中最常用的代码覆盖率工具。11.1 使用JaCoCo收集覆盖率Maven配置 在pom.xml中配置jacoco-maven-plugin。plugin groupIdorg.jacoco/groupId artifactIdjacoco-maven-plugin/artifactId version0.8.11/version executions execution goals goalprepare-agent/goal /goals /execution execution idreport/id phaseverify/phase goals goalreport/goal /goals /execution !-- 可选配置覆盖率检查规则 -- execution idcheck/id goals goalcheck/goal /goals configuration rules rule elementBUNDLE/element limits limit counterLINE/counter valueCOVEREDRATIO/value minimum0.80/minimum !-- 要求行覆盖率至少80% -- /limit /limits /rule /rules /configuration /execution /executions /plugin运行mvn clean verify后JaCoCo会在target/site/jacoco/目录下生成HTML格式的覆盖率报告。Gradle配置 在build.gradle中应用jacoco插件并配置。plugins { id jacoco } jacoco { toolVersion 0.8.11 } test { finalizedBy jacocoTestReport // test任务完成后自动生成报告 } jacocoTestReport { dependsOn test // 报告生成依赖于test任务 reports { xml.required true // CI/CD工具通常需要XML格式 html.required true // 本地查看需要HTML格式 } } // 可选配置覆盖率检查 jacocoTestCoverageVerification { violationRules { rule { limit { minimum 0.80 // 行覆盖率至少80% } } } } // 将检查任务也关联到构建流程 check.dependsOn jacocoTestCoverageVerification11.2 在CI/CD中集成在持续集成流水线如Jenkins GitLab CI GitHub Actions中你通常需要运行测试并生成覆盖率报告XML格式。将覆盖率报告上传到专门的服务器如SonarQube进行长期跟踪和分析。设置质量阈如果覆盖率低于某个值则使构建失败。示例GitHub Actions配置片段- name: Run tests with coverage run: mvn clean verify - name: Upload coverage to SonarCloud uses: SonarSource/sonarcloud-github-actionmaster env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}重要提醒不要盲目追求100%的覆盖率。高覆盖率是好事但它不能保证代码没有bug。要更关注测试用例的质量是否覆盖了核心业务逻辑、边界条件和异常路径。将覆盖率作为一个趋势指标来看待更有价值——确保新增的代码都得到了测试并且整体覆盖率没有持续下降。12. 常见问题与排查技巧实录在实际项目中应用JUnit 5你肯定会遇到一些“坑”。这里记录了一些最常见的问题和解决方法。12.1 测试不运行或找不到测试现象可能原因解决方案Maven下测试完全不执行maven-surefire-plugin版本太旧低于2.22.0升级插件版本至2.22.0。Gradle下测试不执行未配置useJUnitPlatform()在test任务块中加上useJUnitPlatform()。IDE中无法运行单个测试方法项目模块或依赖未正确识别在IDE中执行Maven - Reload Project或Gradle - Refresh。检查src/test/java目录是否被标记为测试源根。控制台输出No tests found测试类或方法不是publicJUnit 4要求或包可见性JUnit 5允许但构建工具配置有冲突确保测试类和方法至少是包可见的。检查构建配置中是否有旧的JUnit 4运行器残留。12.2 依赖冲突与版本问题问题项目中同时存在JUnit 4和JUnit 5的依赖导致行为异常或奇怪的错误。排查运行mvn dependency:tree | grep junit或gradle dependencies | grep junit查看依赖树。确认junit-vintage-engine是否被意外引入。如果不需要运行JUnit 4测试请移除它。确保所有JUnit 5相关组件junit-jupiterjunit-platform-*版本一致。12.3BeforeAll在非静态方法上报错错误BeforeAllmethod must be static unless the test class is annotated with TestInstance(Lifecycle.PER_CLASS)**原因**默认的测试实例生命周期是PER_METHOD每个测试方法都会创建新的测试类实例。BeforeAll需要在所有实例创建前运行因此必须是静态的。解决将BeforeAll方法改为static。或者在测试类上添加TestInstance(Lifecycle.PER_CLASS)注解使整个测试类共享一个实例这样BeforeAll和AfterAll就可以不是静态的了。但需注意测试间的状态隔离。12.4 测试执行顺序问题JUnit 5默认不保证测试方法的执行顺序这是有意为之以确保测试的独立性。如果测试之间有依赖这本身是坏味道或者你为了调试需要固定顺序可以使用TestMethodOrder。TestMethodOrder(MethodOrderer.OrderAnnotation.class) // 按Order注解排序 // TestMethodOrder(MethodOrderer.MethodName.class) // 按方法名排序 // TestMethodOrder(MethodOrderer.Random.class) // 随机排序 class OrderedTests { Test Order(3) void testC() { ... } Test Order(1) void testA() { ... } Test Order(2) void testB() { ... } }强烈建议尽量不要让测试产生依赖。每个测试都应该是独立的、可重复的。如果测试需要共享状态考虑使用BeforeEach来重置状态或者重构你的测试设计。12.5 断言失败信息不清晰当assertEquals失败时如果比较的是复杂对象默认的错误信息可能只是expected: Object12345 but was: Object67890毫无帮助。解决为你的领域对象实现有意义的toString()方法。使用AssertJ这样的断言库它通常能提供更详细的差异对比。在断言中提供自定义的错误信息字符串assertEquals(expected, actual, “Custom message”)JUnit 5会惰性求值这个字符串只在失败时构造。12.6 集成测试中的上下文管理对于Spring Boot等集成测试除了JUnit 5的生命周期还有Spring的上下文生命周期。有时会遇到上下文重复创建导致测试变慢的问题。技巧使用SpringBootTest的webEnvironment属性来优化。对于不需要Web环境的测试使用WebEnvironment.NONE。使用DirtiesContext注解来标记会污染Spring上下文的测试确保后续测试使用干净的上下文但这会增加测试总耗时应谨慎使用。考虑使用TestConfiguration来提供测试专用的Bean定义避免加载完整的生产配置。我个人在实际项目中的体会是JUnit 5的引入和深入使用是一个渐进的过程。不要试图一次性把所有高级特性都用上。先从写好一个干净、独立、意图清晰的普通Test开始然后根据需要逐步引入参数化测试来减少重复用Tag来管理测试套件最后在遇到复杂依赖或特殊需求时才考虑使用扩展模型。保持测试代码的简洁和可维护性其重要性不亚于生产代码。一个好的测试套件应该是项目最可靠的文档和守护者。