JUnit 5 架构、Maven 配置与 JUnit 4 迁移

最后更新: 2026-08-17 10:18:23 +0800 +0800
tags: Java | Junit | Maven | Testing
原文以英文撰写。您正在阅读AI翻译版本。

JUnit 5 将编写测试所使用的 API 与 discovers and executes them. The separation lets Maven, IDEs, and custom launchers run several test models through one interface.

The examples target JUnit 5.13.4. JUnit 6 retains the same architecture, but a project that must remain on JUnit 5 should use the versioned JUnit 5 documentation.

Platform、Jupiter 与 Vintage #

JUnit 5 consists of three components:

ComponentResponsibility
JUnit PlatformDiscovers tests and launches registered test engines. Maven Surefire and IDEs integrate at this level.
JUnit JupiterDefines the JUnit 5 programming model, extension API, and test engine.
JUnit VintageRuns JUnit 3 and JUnit 4 tests on the Platform during migration.

A Jupiter test imports @Test and assertions from org.junit.jupiter.api. Surefire asks the Platform to discover tests, then junit-jupiter-engine executes the discovered methods. Application code does not depend on any JUnit component because test dependencies use Maven’s test scope.

最小 Maven 项目 #

The following layout keeps production and test classes separate:

text
junit-hello/
├── pom.xml
└── src
    ├── main/java/com/example/Greeter.java
    └── test/java/com/example/GreeterTest.java

Use org.junit.jupiter:junit-jupiter for the smallest maintainable POM. The artifact aggregates the Jupiter API, parameterized-test support, and runtime engine.

xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>junit-hello</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <junit.version>5.13.4</junit.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.4</version>
      </plugin>
    </plugins>
  </build>
</project>
java
package com.example;

public final class Greeter {
    public String greet(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name must not be blank");
        }
        return "Hello, " + name;
    }
}
java
package com.example;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class GreeterTest {
    private final Greeter greeter = new Greeter();

    @Test
    void greetsByName() {
        assertEquals("Hello, Ada", greeter.greet("Ada"));
    }

    @Test
    void rejectsBlankNames() {
        var error = assertThrows(
                IllegalArgumentException.class,
                () -> greeter.greet(" "));

        assertEquals("name must not be blank", error.getMessage());
    }
}

Run the test through Maven:

sh
mvn test

Jupiter test classes and methods may have package-private visibility. Test methods must not be private or static, although lifecycle and extension rules permit static in specific places.

选择 JUnit 工件 #

Add another artifact only when its capability is required:

ArtifactWhen to Add It
junit-jupiterPrefer the aggregate dependency for an application test suite. It already includes API, parameters, and engine.
junit-jupiter-apiCompile tests or extensions against the API while managing runtime engines separately.
junit-jupiter-engineExecute Jupiter tests when using fine-grained dependencies instead of junit-jupiter. Use test runtime scope.
junit-jupiter-paramsAdd @ParameterizedTest support to a fine-grained setup. The aggregate dependency already includes it.
junit-vintage-engineRun unchanged JUnit 3 or JUnit 4 tests through the Platform.
junit-jupiter-migrationsupportTemporarily adapt selected JUnit 4 rules and @Ignore while converting tests to Jupiter.
junit-platform-suiteDeclare suites with Platform suite annotations.
junit-platform-launcherDiscover and execute tests programmatically, or implement test tooling. Application test suites do not need it directly.
junit-platform-console-standaloneRun tests outside Maven or an IDE from one executable JAR. Keep the JAR outside the project POM.

Import the JUnit BOM when a build declares several JUnit artifacts because the BOM keeps Platform, Jupiter, and Vintage versions compatible:

xml
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>${junit.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Artifact declarations covered by the BOM can omit <version>.

An explicit junit-platform-launcher dependency was necessary with Surefire releases before 3.0. Current Surefire 3.x releases align the launcher with the Platform on the test runtime classpath, so adding a launcher to an ordinary test suite creates another version to manage without adding test capability.

Jupiter 编程模型 #

Jupiter provides focused annotations for common test structures:

  • @BeforeEach and @AfterEach establish per-test fixtures.
  • @BeforeAll and @AfterAll establish class-level fixtures.
  • @ParameterizedTest executes one test body with data from @ValueSource, @CsvSource, @MethodSource, or another argument source.
  • @Nested groups related cases while preserving lifecycle semantics.
  • @Tag classifies tests for build-time inclusion or exclusion.
  • @ExtendWith registers extensions for parameter resolution, callbacks, conditional execution, and other cross-cutting behavior.

Tests should assert observable results rather than execution order. Jupiter does not guarantee a self-evident order from method names, and ordered tests share state that makes failures dependent on earlier cases.

不做修改地运行 JUnit 4 测试 #

Vintage provides the lowest-risk first migration step because Jupiter and JUnit 4 use different Java packages and can coexist in one module:

xml
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.13.2</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.vintage</groupId>
  <artifactId>junit-vintage-engine</artifactId>
  <version>${junit.version}</version>
  <scope>test</scope>
</dependency>

Surefire then discovers Jupiter and JUnit 4 tests through the Platform. Keep Vintage only while old tests remain because a successful Vintage run does not exercise Jupiter’s programming or extension model.

转换 JUnit 4 测试 #

Common source mappings are:

JUnit 4Jupiter
@Before / @After@BeforeEach / @AfterEach
@BeforeClass / @AfterClass@BeforeAll / @AfterAll
@Ignore@Disabled
@Category@Tag
@RunWithOne or more @ExtendWith extensions
@Rule / @ClassRuleAn extension or Jupiter-native fixture
@Test(expected = E.class)assertThrows(E.class, executable)

@RunWith to @ExtendWith is a conceptual mapping rather than a mechanical rename because a JUnit 4 class can have one runner while Jupiter composes multiple extensions.

JUnit 4 places an optional assertion message first:

java
assertEquals("unexpected greeting", expected, actual);

Jupiter places the message last:

java
assertEquals(expected, actual, "unexpected greeting");

Existing org.junit.Assert calls can remain while annotations and lifecycle code move to Jupiter. Converting assertions separately reduces the size of each migration change.

Rules need individual treatment. The junit-jupiter-migrationsupport artifact adapts ExternalResource, Verifier, and ExpectedException rule families; @EnableRuleMigrationSupport activates those adapters. @EnableJUnit4MigrationSupport also recognizes JUnit 4 @Ignore. Other custom rules require a Jupiter extension or a direct fixture rewrite.

A controlled migration therefore follows three stages:

  1. Add Vintage and establish one Platform-based test command.
  2. Convert classes to Jupiter, replacing runners and rules according to their behavior rather than their names.
  3. Remove Vintage, JUnit 4, and migration support after the build contains no JUnit 3 or JUnit 4 tests.

参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)