JUnit 5 单元测试框架深度解析:从基础注解到扩展模型与Mockito集成
2026/7/17 8:29:50 网站建设 项目流程

1. 项目概述:为什么今天还需要深入JUnit 5?

如果你是一名Java开发者,无论你是刚入行的新人,还是已经写了多年业务代码的老手,单元测试都是一个绕不开的话题。而一提到Java单元测试,JUnit几乎是条件反射般的答案。但很多人对JUnit的认知,可能还停留在JUnit 4那个@Test@Before@After的时代。当项目从JUnit 4迁移到JUnit 5,或者新建项目直接使用JUnit 5时,不少人会感到一丝困惑:这些注解怎么变了?这个@BeforeEach是干嘛的?assertThat去哪了?更别提那些听起来很高级的扩展模型、动态测试了。

这正是我写这篇解析的初衷。JUnit 5在2017年正式发布,它不是一个简单的版本迭代,而是一次彻底的重构和理念升级。它由三个不同的子模块组成:JUnit Platform(提供在JVM上启动测试框架的基础)、JUnit Jupiter(提供新的编程模型和扩展模型)和JUnit Vintage(提供对JUnit 3/4的兼容)。这意味着,它从底层架构上就为现代Java开发(尤其是Java 8及以后的Lambda、模块化等特性)和更复杂的测试场景做好了准备。

在日常工作中,我见过太多测试代码写得像“走过场”,仅仅是为了满足覆盖率指标,而丧失了测试本身作为“设计工具”和“文档”的价值。JUnit 5引入的一系列新特性,比如参数化测试、动态测试、嵌套测试、测试接口默认方法等,本质上是在鼓励我们写出表达力更强、更易于维护、更能反映业务意图的测试代码。掌握它,不仅能提升你的测试效率,更能潜移默化地改善你的代码设计思维。接下来,我将从最基础的搭建开始,一直深入到那些能解决实际痛点的“高级玩法”,带你重新认识这个强大的测试框架。

2. 环境搭建与基础注解:告别JUnit 4的思维定式

很多教程一上来就讲注解,但我觉得,先搞定环境,消除配置的恐惧感更重要。JUnit 5的依赖和JUnit 4有显著不同,这也是第一个容易踩坑的地方。

2.1 构建工具依赖配置

如果你使用Maven,在pom.xml中需要引入以下依赖。关键点在于,JUnit 5的junit-jupiter依赖是一个聚合依赖,它通常就足够了。但为了运行测试,你还需要配置maven-surefire-plugin插件来确保兼容性。

<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> <!-- 请使用最新稳定版本 --> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M7</version> </plugin> </plugins> </build>

为什么这么配?junit-jupiter依赖包含了Jupiter的API、引擎和参数化测试等常用模块。而Surefire插件从2.22.0版本开始原生支持JUnit 5,无需额外配置就能识别@Test等注解。如果你用的是Gradle,配置更简单:

dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' } test { useJUnitPlatform() }

注意:千万不要再混用JUnit 4和JUnit 5的依赖。如果老项目迁移,需要测试旧的JUnit 4代码,可以额外引入junit-vintage-engine,但新写的测试类务必使用JUnit 5的注解和断言。

2.2 核心生命周期注解:语义化的巨大进步

JUnit 5的注解包名从org.junit变为了org.junit.jupiter.api,这是一个明显的区隔。最核心的生命周期注解变化,体现了更好的语义表达:

  • @Test: 这个没变,还是用来标记一个测试方法。但它的可访问性可以是package-private了,这符合“测试是类实现的一部分”的理念。
  • @BeforeEach/@AfterEach: 替代了JUnit 4的@Before/@After每个测试方法执行之前/之后都会运行。这个名字更精确,避免了“Before/After是每个还是所有?”的歧义。
  • @BeforeAll/@AfterAll: 替代了JUnit 4的@BeforeClass/@AfterClass。在所有测试方法执行之前/之后运行一次。这里有个关键变化:这两个注解修饰的方法必须是static(除非使用@TestInstance(Lifecycle.PER_CLASS)注解改变测试实例生命周期)。
import org.junit.jupiter.api.*; class LifecycleTest { @BeforeAll static void initAll() { System.out.println("初始化全局资源,例如数据库连接池"); } @BeforeEach void init() { System.out.println("为当前测试方法准备干净的数据"); } @Test void testOne() { System.out.println("执行测试一"); } @Test void testTwo() { System.out.println("执行测试二"); } @AfterEach void tearDown() { System.out.println("清理当前测试产生的数据"); } @AfterAll static void tearDownAll() { System.out.println("关闭全局资源"); } }

运行上述测试,输出顺序会清晰地展示生命周期。这种语义化的改进,让测试代码的意图一目了然。

2.3 断言(Assertions)与假设(Assumptions):更强大的验证工具

JUnit 5的断言API放在了org.junit.jupiter.api.Assertions类中,它全面支持Lambda表达式,使得断言失败时的错误信息可以延迟计算,提升了性能。

import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class AssertionsTest { @Test void standardAssertions() { assertEquals(2, 1 + 1); assertTrue(1 < 2, "这个失败消息可以自定义"); // 使用Lambda,只有断言失败时才会执行消息构造逻辑 assertTrue(1 < 2, () -> "这是一个" + "昂贵的" + "字符串拼接操作"); } @Test void groupedAssertions() { // 分组断言:所有断言都会执行,最后一起报告失败 Person person = new Person("John", "Doe"); assertAll("person properties", () -> assertEquals("John", person.getFirstName()), () -> assertEquals("Doe", person.getLastName()) ); } @Test void exceptionTesting() { // 异常断言:写法更符合流式API风格 IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> { throw new IllegalArgumentException("a message"); } ); assertEquals("a message", exception.getMessage()); } }

分组断言(assertAll是我强烈推荐的一个特性。在JUnit 4中,如果第一个断言失败,后面的就不会执行,你无法知道其他属性是否正确。而assertAll会执行所有子断言,最后给出一个汇总报告,对于验证一个对象的多个属性非常有用。

假设(Assumptions)是另一个实用工具。它用于在特定条件不满足时跳过测试,而不是让测试失败。这常用于“仅在特定环境下运行”的测试。

@Test void testOnlyOnCiServer() { // 假设环境变量`ENV`的值是`CI`,才执行后续测试逻辑 Assumptions.assumeTrue("CI".equals(System.getenv("ENV"))); // ... 只有在CI服务器上才会执行的测试代码 } @Test void testOnlyOnDeveloperWorkstation() { // 假设条件不满足,整个测试方法会被标记为`Aborted`(已中止),而不是失败 Assumptions.assumeFalse("CI".equals(System.getenv("ENV"))); // ... 只有在开发机上才会执行的测试代码 }

假设和断言的区分很重要:断言是对测试逻辑结果的验证,失败意味着功能有问题;假设是对测试执行前提的检查,不满足意味着当前环境不适合运行此测试。合理使用假设可以让测试集更清晰。

3. 核心进阶特性:让测试代码更具表达力

掌握了基础之后,JUnit 5那些真正提升效率和生产力的特性才值得深入。它们能让你摆脱重复、僵化的测试代码编写模式。

3.1 参数化测试:告别重复的测试方法

如果你需要对同一个方法用多组不同的输入和期望输出进行测试,在JUnit 4时代,你可能需要写多个@Test方法,或者使用@Parameterizedrunner,但配置繁琐。JUnit 5的@ParameterizedTest是核心武器,它需要额外依赖junit-jupiter-params

<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency>

参数化测试的强大之处在于它提供了多种数据源注解:

1.@ValueSource: 最简单的数据源,支持基本类型、String和Class。

@ParameterizedTest @ValueSource(ints = {1, 3, 5, -3, 15}) void testIsOdd(int number) { assertTrue(MathUtils.isOdd(number)); }

2.@CsvSource/@CsvFileSource: 处理CSV格式数据,适合多参数场景。

@ParameterizedTest @CsvSource({ "apple, 5", "banana, 6", "'hello,world', 11" // 包含逗号的值需要用单引号包裹 }) void testStringLength(String input, int expectedLength) { assertEquals(expectedLength, input.length()); } // 或者从类路径下的CSV文件读取 @ParameterizedTest @CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1) void testFromFile(String name, int age) { // ... }

3.@MethodSource: 最灵活的方式,指定一个返回Stream、Collection等的静态工厂方法作为数据源。

@ParameterizedTest @MethodSource("provideStringsForLengthTest") void testWithMethodSource(String input, int expectedLength) { assertEquals(expectedLength, input.length()); } static Stream<Arguments> provideStringsForLengthTest() { return Stream.of( Arguments.of("apple", 5), Arguments.of("banana", 6), Arguments.of(null, 0) // 甚至可以测试边界情况如null ); }

实操心得@MethodSource是我最常用的方式,因为它可以将复杂的测试数据准备逻辑封装在一个方法里,数据来源可以是数据库、文件或任何计算过程,可读性和可维护性最好。对于简单的几组数据,用@CsvSource更直观。

3.2 动态测试:运行时生成测试用例

这是JUnit 5的一个革命性特性。静态的@Test@ParameterizedTest在编译时就必须确定所有测试用例。而@TestFactory允许你在运行时动态生成测试用例。这对于需要遍历复杂数据结构、或根据外部资源(如文件系统、数据库查询结果)生成测试的场景极其有用。

一个@TestFactory方法必须返回StreamCollectionIterableIterator类型的DynamicNode(通常是DynamicTest)。

import org.junit.jupiter.api.*; class DynamicTestsDemo { @TestFactory Stream<DynamicTest> dynamicTestsFromStream() { // 输入数据流 List<String> inputList = Arrays.asList("apple", "banana", "orange"); // 预期的输出 List<Integer> outputList = Arrays.asList(5, 6, 6); // 构建动态测试流 return inputList.stream() .map(word -> DynamicTest.dynamicTest( "Test length of: " + word, // 动态生成的测试名 () -> { int index = inputList.indexOf(word); assertEquals(outputList.get(index), word.length()); } )); } @TestFactory Collection<DynamicTest> generateTestsFromDatabase() { // 假设从数据库读取了一批测试配置 List<TestConfig> configs = fetchTestConfigsFromDB(); return configs.stream() .map(config -> DynamicTest.dynamicTest( "Test config: " + config.getId(), () -> executeTestWithConfig(config) )) .collect(Collectors.toList()); } }

应用场景:想象一下,你需要测试一个文件解析器对目录下所有特定格式文件的支持情况。使用动态测试,你可以遍历目录,为每个文件生成一个独立的测试用例,测试报告会清晰列出每个文件的通过与否,而不是一个笼统的“解析测试”。

注意事项:动态测试不支持生命周期注解(@BeforeEach等)。每个动态测试的执行都是独立的。如果需要在动态测试前后执行准备和清理工作,需要在@TestFactory方法内部或DynamicTest的可执行代码中手动处理。

3.3 嵌套测试与测试接口:更好的测试组织

随着测试类越来越庞大,逻辑上相关的测试方法混杂在一起,可读性变差。@Nested注解可以帮助我们按逻辑层次组织测试。

class StackTest { Stack<Object> stack; @Test void isEmpty() { /* ... */ } @Nested class WhenNew { @BeforeEach void createNewStack() { stack = new Stack<>(); } @Test void isEmpty() { assertTrue(stack.isEmpty()); } @Nested class AfterPushing { String anElement = "an element"; @BeforeEach void pushAnElement() { stack.push(anElement); } @Test void isNotEmpty() { assertFalse(stack.isEmpty()); } @Test void returnElementWhenPopped() { /* ... */ } } } }

嵌套测试类可以拥有自己的生命周期方法(@BeforeEach等),并且可以继承外层类的成员变量。这种结构像在写一篇技术文档,清晰地描述了“当对象处于某种状态时,它应该表现出何种行为”。在IDE中,它们通常以可折叠的树形结构展示,一目了然。

测试接口默认方法是另一个组织代码的利器。你可以将重复的测试逻辑(例如,某个接口的所有实现都应满足的契约)定义在接口的default方法中,并用@Test@BeforeEach等注解标记。然后让具体的测试类实现这个接口。

interface TestLifecycleLogger { @BeforeAll static void beforeAll() { System.out.println("Before all tests in the interface"); } @BeforeEach default void beforeEachTest() { System.out.println("Before each test (from interface)"); } } interface EqualsContractTest<T> { T createValue(); T createNotEqualValue(); @Test default void valueEqualsItself() { T value = createValue(); assertEquals(value, value); } @Test default void valueDoesNotEqualDifferentValue() { T value1 = createValue(); T value2 = createNotEqualValue(); assertNotEquals(value1, value2); } } class StringTests implements TestLifecycleLogger, EqualsContractTest<String> { @Override public String createValue() { return "foo"; } @Override public String createNotEqualValue() { return "bar"; } // 还可以添加String特有的测试 @Test void specificStringTest() { /* ... */ } }

这种方式实现了测试逻辑的复用和契约的集中定义,非常适合为API或抽象类定义一套通用的合规性测试套件。

4. 扩展模型(Extension Model):定制你的测试引擎

JUnit 5最强大的设计之一就是用扩展模型(Extension Model)取代了JUnit 4的Runner机制。Runner是“独占”的,一个测试类只能指定一个Runner,这限制了功能的组合。而扩展模型是“可组合”的,你可以通过@ExtendWith注解注册多个扩展,它们像插件一样,在测试生命周期的不同阶段介入。

JUnit 5提供了一些内置扩展,但理解其模型后,你可以编写自己的扩展来解决特定问题。

4.1 常用内置扩展

  • @TempDir: 一个字段级注解,用于在测试方法或类执行期间创建临时文件和目录,并在执行后自动清理。这解决了测试中文件资源管理的难题。

    @Test void writeToTempFile(@TempDir Path tempDir) throws IOException { Path testFile = tempDir.resolve("test.txt"); Files.writeString(testFile, "hello"); assertEquals("hello", Files.readString(testFile)); } // 测试结束后,tempDir及其内容会被自动删除
  • @Timeout: 方法级或类级注解,为测试执行设置超时时间。防止因死锁、无限循环等问题导致测试套件挂起。

    @Test @Timeout(5) // 单位:秒 void failsIfExecutionTimeExceedsFiveSeconds() throws InterruptedException { Thread.sleep(10_000); // 这个测试会失败 }

4.2 理解扩展点与自定义扩展

JUnit Jupiter定义了一系列扩展点API(接口),如:

  • BeforeAllCallback/AfterAllCallback
  • BeforeEachCallback/AfterEachCallback
  • BeforeTestExecutionCallback/AfterTestExecutionCallback
  • ParameterResolver: 用于动态解析测试方法或构造函数的参数。
  • TestExecutionExceptionHandler: 用于处理测试执行过程中抛出的异常。
  • InvocationInterceptor: 更底层的拦截器(高级用法)。

编写一个自定义扩展,就是实现这些接口。例如,我们创建一个简单的扩展,在每个测试方法执行前后打印日志:

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final Logger logger = LoggerFactory.getLogger(TimingExtension.class); 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; logger.info("方法 [{}] 执行耗时 {} ms", context.getRequiredTestMethod().getName(), duration); } private ExtensionContext.Store getStore(ExtensionContext context) { // Store用于在同一测试执行上下文中存储和检索数据 return context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod())); } } // 使用扩展 @ExtendWith(TimingExtension.class) class MyServiceTest { @Test void someTest() { /* ... */ } }

更实用的例子:数据库事务回滚扩展。在集成测试中,我们通常希望每个测试方法在一个独立的事务中运行,测试后回滚,避免污染数据库。虽然Spring Test提供了这样的功能,但理解原理后,我们可以用JUnit 5扩展模拟一个简化版:

public class DatabaseTransactionExtension implements BeforeEachCallback, AfterEachCallback { @Override public void beforeEach(ExtensionContext context) { Connection conn = getConnection(); // 获取数据库连接 conn.setAutoCommit(false); // 开启事务 // 将连接存储到上下文中,供测试方法或`ParameterResolver`使用 context.getStore(ExtensionContext.Namespace.GLOBAL).put("TEST_TRANSACTION_CONNECTION", conn); } @Override public void afterEach(ExtensionContext context) { Connection conn = context.getStore(ExtensionContext.Namespace.GLOBAL) .remove("TEST_TRANSACTION_CONNECTION", Connection.class); if (conn != null) { conn.rollback(); // 回滚事务 conn.close(); } } // 还可以实现一个`ParameterResolver`,将Connection注入到测试方法参数中 static class ConnectionResolver implements ParameterResolver { @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameterContext.getParameter().getType().equals(Connection.class); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return extensionContext.getStore(ExtensionContext.Namespace.GLOBAL) .get("TEST_TRANSACTION_CONNECTION"); } } }

实操心得:扩展模型的学习曲线稍陡,但它是将测试中的横切关注点(如日志、事务、安全性、模拟数据注入)模块化的终极工具。在大型项目中,将通用的测试基础设施封装成扩展,能极大提升测试代码的整洁度和一致性。开始时,可以多参考junit-jupiter源码中内置扩展的实现。

5. 与Mockito等Mock框架的深度集成

单元测试的核心是“隔离”,我们通常使用Mock框架(如Mockito)来模拟依赖对象的行为。JUnit 5与Mockito的集成比JUnit 4更优雅。

5.1 使用MockitoExtension

最直接的方式是使用Mockito为JUnit 5提供的官方扩展。首先添加依赖:

<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>5.7.0</version> <!-- 使用与Mockito核心兼容的版本 --> <scope>test</scope> </dependency>

然后在测试类上使用@ExtendWith(MockitoExtension.class)

@ExtendWith(MockitoExtension.class) class ServiceTestWithMockito { @Mock private DependencyRepository repositoryMock; // 被Mock的依赖 @InjectMocks private ServiceUnderTest service; // 待测服务,Mock会自动注入 @Test void testWithMock() { // 1. 定义Mock行为 when(repositoryMock.findById(anyLong())).thenReturn(Optional.of(new Entity("test"))); // 2. 执行待测方法 Result result = service.process(1L); // 3. 验证结果和交互 assertNotNull(result); verify(repositoryMock).findById(1L); // 验证方法被以特定参数调用 } }

@Mock创建Mock对象,@InjectMocks创建待测类的实例,并自动将@Mock(或@Spy)标注的字段注入进去。这种方式避免了手动调用MockitoAnnotations.openMocks(testClass)的样板代码。

5.2 参数解析器(ParameterResolver)与Mock结合

JUnit 5的扩展模型允许你将Mock对象作为测试方法的参数传入,这进一步提升了灵活性。MockitoExtension本身就注册了这样的ParameterResolver

@ExtendWith(MockitoExtension.class) class ParameterInjectionTest { @Test void testWithMockAsParameter(@Mock DependencyRepository repositoryMock) { // repositoryMock 已经是一个初始化好的Mock对象 ServiceUnderTest service = new ServiceUnderTest(repositoryMock); // ... 测试逻辑 } @Test void testWithMultipleMocks(@Mock DependencyRepository repo, @Mock AnotherService anotherService) { // 可以注入多个Mock } }

这种方式让测试方法更加自包含,依赖关系一目了然,特别适合在需要为不同测试方法配置不同Mock行为的场景。

5.3 严格Stubbing与验证

Mockito与JUnit 5结合,可以更好地利用Mockito的一些高级特性,比如严格Stubbing。在测试类上添加@MockitoSettings(strictness = Strictness.STRICT_STUBS)可以开启严格模式。在此模式下,不必要的Stubbing(即定义了但未在测试中使用的when(...).thenReturn(...))会导致测试失败,这有助于保持测试的简洁和准确,避免遗留无用的Mock配置。

@ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.STRICT_STUBS) class StrictMockTest { // ... }

6. 测试报告、过滤与执行顺序

当测试套件变得庞大时,如何管理和控制测试的执行就变得重要。

6.1 标签(Tag)与测试过滤

你可以使用@Tag注解为测试类或方法打上标签,然后根据标签来过滤执行哪些测试。

class TaggedTests { @Test @Tag("fast") void fastTest() { /* 快速单元测试 */ } @Test @Tag("slow") @Tag("integration") // 可以打多个标签 void integrationTest() { /* 耗时的集成测试 */ } }

在Maven中过滤

<plugin> <artifactId>maven-surefire-plugin</artifactId> <configuration> <!-- 只运行标记为‘fast’的测试 --> <groups>fast</groups> <!-- 排除标记为‘slow’的测试 --> <excludedGroups>slow</excludedGroups> </configuration> </plugin>

在Gradle中过滤

test { useJUnitPlatform { includeTags 'fast' excludeTags 'slow' } }

在IDE中:大多数IDE(如IntelliJ IDEA)都支持在运行配置中按标签过滤。这非常有用,例如,在本地开发时只运行“fast”标签的单元测试,而在CI流水线中才运行所有包括“integration”标签的测试。

6.2 自定义测试显示名称

@DisplayName注解可以为测试类或方法提供一个更易读的名称,这个名称会出现在测试报告和IDE中,替代默认的方法名。

@DisplayName("计算器测试套件") class CalculatorTests { @Test @DisplayName("1 + 1 应该等于 2") void onePlusOneEqualsTwo() { assertEquals(2, 1 + 1); } @Test @DisplayName("除以零应该抛出 ArithmeticException") void divisionByZeroShouldThrowException() { assertThrows(ArithmeticException.class, () -> 1 / 0); } }

你甚至可以使用@DisplayNameGeneration指定一个生成器,或者使用@ParameterizedTestname属性来为参数化测试的每个用例生成动态名称,这大大提升了测试报告的可读性。

6.3 测试执行顺序

默认情况下,JUnit 5并不保证测试方法的执行顺序,这是有意为之的,以确保测试的独立性。但有时,出于性能考虑(比如需要昂贵的外部资源初始化)或遗留代码原因,你可能需要指定顺序。可以使用@TestMethodOrder注解。

@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // 使用@Order注解指定顺序 class OrderedTests { @Test @Order(1) void firstTest() { System.out.println("第一个执行"); } @Test @Order(2) void secondTest() { System.out.println("第二个执行"); } @Test @Order(3) void thirdTest() { System.out.println("第三个执行"); } }

除了OrderAnnotation,还有MethodName(按方法名字母顺序)、Random(随机)等排序器。但务必谨慎使用测试顺序,依赖顺序的测试往往是脆弱的,不易维护。优先考虑使用@BeforeAll进行一次性初始化,或者重构测试使其完全独立。

7. 从JUnit 4迁移到JUnit 5:平滑升级指南

对于已有大量JUnit 4测试的项目,全盘重写是不现实的。JUnit 5通过junit-vintage-engine模块提供了良好的向后兼容性。

第一步:添加依赖确保你的pom.xmlbuild.gradle中包含了JUnit 5的依赖(如第2.1节所述),并且保留了现有的JUnit 4依赖(如junit:junit:4.13.2)。同时添加junit-vintage-engine

<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency>

这样,你的构建工具(Surefire)就能同时运行JUnit 4(通过Vintage引擎)和JUnit 5(通过Jupiter引擎)的测试。

第二步:并行运行与逐步迁移现在,新旧测试可以共存。你可以:

  1. 新写的测试全部使用JUnit 5(org.junit.jupiter.api包)。
  2. 旧的JUnit 4测试无需修改,继续运行。
  3. 在时间允许时,逐步将重要的、经常修改的JUnit 4测试类迁移到JUnit 5。

第三步:迁移单个测试类迁移一个类时,通常需要做以下更改:

  1. 包导入:将org.junit下的导入改为org.junit.jupiter.api
  2. 注解替换
    • @Before->@BeforeEach
    • @After->@AfterEach
    • @BeforeClass->@BeforeAll(同时将方法改为static)
    • @AfterClass->@AfterAll(同时将方法改为static)
    • @Ignore->@Disabled
  3. 断言迁移
    • org.junit.Assert->org.junit.jupiter.api.Assertions
    • 注意方法名变化,如assertThat在JUnit 5中不再直接提供,建议使用Assertions自带的断言或结合Hamcrest、AssertJ等第三方断言库。
  4. 规则(Rule)迁移:JUnit 4的@Rule@ClassRule在JUnit 5中被扩展模型取代。常见的规则(如TemporaryFolderExpectedException)在JUnit 5中都有对应的替代品(@TempDirassertThrows)。对于自定义规则,需要重写为扩展(Extension)。
  5. Runner迁移@RunWith@ExtendWith取代。例如,@RunWith(SpringRunner.class)变为@ExtendWith(SpringExtension.class)@SpringBootTest等Spring Boot测试注解已经集成了JUnit 5支持,通常无需显式添加@ExtendWith

常见问题排查

  • 测试不运行:检查构建插件配置(如Maven Surefire版本≥2.22.0),检查依赖冲突。
  • 迁移后测试失败:重点检查断言逻辑和异常测试。JUnit 5的assertThrows会返回异常对象供进一步验证,而JUnit 4的@Test(expected=...)ExpectedException规则行为略有不同。
  • 生命周期方法不执行:确认@BeforeAll/@AfterAll方法是否为static,或者测试类是否使用了@TestInstance(Lifecycle.PER_CLASS)

迁移过程可能是渐进的,核心目标是让新的测试享受JUnit 5的新特性,同时保证旧的测试资产继续发挥作用。利用好并行运行期,可以大大降低迁移风险。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询