JUnit 5 教程 (含示例)

JUnit 5 教程 讨论了 JUnit 框架如何采用 Java 8 编码风格 和新版本 5 中的其他一些新特性。它还解释了 JUnit 5 与 JUnit 4 的区别

JUnit 是 Java 应用程序最常用的测试框架,也是 Java 中开发单元测试的事实标准。 它是一个开源软件,托管在 GitHub 上,并具有 Eclipse 公共许可证。

JUnit 4 已经完美地完成它的工作了很长时间。其间,JDK 8 带来了令人着迷的功能,最值得注意的是 lambda 表达式。 JUnit 5 旨在采用 Java 8 编码风格;这就是为什么 Java 8 是在 JUnit 5 中创建和执行测试的最低要求版本(尽管可以运行用 JUnit 3 或 JUnit 4 编写的测试以实现向后兼容性)。

1. 术语

在深入研究概念之前,让我们澄清 JUnit 测试中常用的一些术语。

术语描述
容器测试树 中的一个节点,包含其他容器或测试。 例如,一个测试类。它可以包含其他嵌套测试类以及测试方法。
测试测试树中的一个节点,在执行时验证预期行为。 例如,一个测试方法。
生命周期方法使用 @BeforeAll@AfterAll@BeforeEach@AfterEach 注解或元注解的方法。
测试类包含至少一个 测试方法的顶级类。它不能是 抽象 类。
测试方法使用 @Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate 注解或元注解的方法。

2. JUnit 5 架构

从历史上看,JUnit 4 是单体的,未设计成与流行的构建工具(Maven 和 Gradle)和 IDE(Eclipse、NetBeans 和 IntelliJ)交互。这些工具与 JUnit 4 紧密耦合,并且通常依赖于反射来获取必要的信息。 这带来了挑战,例如,如果 JUnit 的设计者决定更改一个 私有 变量的名称,那么这种更改可能会影响访问它的工具。

JUnit 5 将模块化方法引入了框架,并且能够允许 JUnit 与不同的程序客户端交互,这些客户端使用不同的工具和 IDE。 它以 API 的形式引入了以下关注点分离的逻辑

  • 一个用于编写测试的 API,主要供开发人员使用
  • 一种发现和运行测试的机制
  • 一个API,允许与 IDE 和工具轻松交互,并从它们运行测试

因此,JUnit 5 由来自三个不同子项目的几个不同模块组成

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform为了能够启动 JUnit 测试,IDE、构建工具或插件需要包含并扩展平台 API。它定义了 TestEngineAPI,用于开发在平台上运行的新测试框架。它还提供了一个控制台启动器,用于从命令行启动平台以及 Gradle 和 Maven 的构建插件。
  • JUnit Jupiter:它包括用于编写测试的新编程和扩展模型。它拥有所有新的 JUnit 注解和 TestEngine 实现,用于运行使用这些注解编写的测试。
  • JUnit Vintage:它的主要目的是支持在 JUnit 5 平台上运行 JUnit 3 和 JUnit 4 编写的测试。 它是向后兼容性的体现。它需要存在 JUnit 4.12 或更高版本在类路径或模块路径上。

3. JUnit 5 Maven 依赖项

JUnit 5 的版本是模块化的;您不能简单地将一个 jar 文件添加到项目的编译类路径和执行类路径中。您可以通过包含项目中所需的依赖项来在 Maven 或 Gradle 项目中使用 JUnit 5。

让我们先简要了解一下实际应用程序中常用的工件

  • junit-jupiter-api:这是主模块,其中包含所有核心注解,例如 @Test生命周期方法 注解和 断言
  • junit-jupiter-engine:它具有测试引擎实现,需要在运行时执行测试。
  • junit-jupiter-params:它提供对参数化测试的支持。
  • junit-platform-suite:它提供 @Suite 支持,使得遗留 JUnit 4 的 JUnitPlatform 运行器过时。
  • junit-vintage-engine:它包含引擎实现,用于执行用 JUnit 3 或 4 编写的测试。为此,当然还需要 JUnit 3 或 4 jar。
<properties>
    <junit.jupiter.version>5.11.0</junit.jupiter.version>
    <junit.platform.version>1.11.0</junit.platform.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>${junit.jupiter.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>${junit.jupiter.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-params</artifactId>
        <version>${junit.jupiter.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <version>${junit.platform.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>
dependencies {
    testRuntime("org.junit.jupiter:junit-jupiter-api:5.11.0")
    testRuntime("org.junit.jupiter:junit-jupiter-engine:5.11.0")
    testRuntime("org.junit.jupiter:junit-jupiter-params:5.11.0")
    testRuntime("org.junit.platform:junit-platform-suite:1.11.0")
}
test {
    useJUnitPlatform()
}
JUnit 5 模块

更多信息:Maven 示例 | Gradle 示例

要能够从命令行运行测试,请确保您的 pom.xml 配置文件包含 Maven Surefire 插件的 JUnit 提供程序依赖项。

<build>
  <plugins>
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>2.22.2</version>
    </plugin>
  </plugins>
</build>

现在我们在项目文件夹中打开一个命令提示符(包含 pom.xml 文件),并运行此命令

mvn test

此命令将获取 Java 源代码,对其进行编译并通过执行 /src/main/test 目录中的所有测试来对其进行测试。

4. JUnit 5 注解

4.1. 内置注解

JUnit 5 提供了以下内置注解来编写测试。

带有 @BeforeAll、 @AfterAll、 @BeforeEach 或 @AfterEach 注解或元注解的方法称为 生命周期方法

注解描述
@BeforeEach带有注解的方法将在测试类中的每个测试方法之前运行。
@AfterEach带有注解的方法将在测试类中的每个测试方法之后运行。
@BeforeAll带有注解的方法必须是 static,它将在测试类中的所有测试方法之前运行。
@AfterAll带有注解的方法将在测试类中的所有测试方法之后运行。该方法必须是 static。
@AutoClose带有注解的字段表示一个资源,该资源将在测试执行后自动关闭。
@Test它用于将方法标记为 JUnit 测试。
@TestInstance用于配置带有注解的测试类的 测试实例生命周期
@DisplayName用于为测试类或测试方法提供任何自定义显示名称。
@Disabled它用于禁用或忽略测试套件中的测试类或测试方法。
@Nested用于创建嵌套测试类。
@Tag使用标签标记测试方法或测试类,用于测试发现和过滤。
@TestFactory将方法标记为动态测试的测试工厂。
@ParameterizedTest表示该方法是参数化测试。
@RepeatedTest表示该方法是重复测试的测试模板。
@TestClassOrder用于配置带有注解的测试类的 @Nested 测试类的测试类执行顺序。
@TestMethodOrder用于配置带有注解的测试类的测试方法执行顺序,类似于 JUnit 4 的 @FixMethodOrder
@Timeout用于如果其执行超过给定持续时间,则使测试、测试工厂、测试模板或生命周期方法失败。
@TempDir用于通过字段注入或生命周期方法或测试方法中的参数注入提供临时目录。

使用 JUnit 5 注解的典型测试类如下

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

public class AppTest {


  @BeforeAll
  static void setup() {
    // setup the common resource(s) for all tests
  }

  @AfterAll 
  static void tearDown() {
     // close the common resource(s) for all tests
  }

  @Test        
  void testMethod_expect_true() {

    boolean result =  systemUnderTest.someMethod();
    assertTrue(result);
  }

  @Test
  @Disabled     
  void testMethod_temporary_disabled() {
      assertEquals(2, 1, "2 is not equal to 1");  //Not executed because it is disabled
  }
}

4.2 自定义组合注解

我们还可以创建组合注解,它将自动继承其元注解的语义。

例如,不必复制和粘贴@Tag(“developement”),我们可以创建如下的组合注解 @Dev

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("developement")
public @interface Dev {
  //...
}

然后,我们可以将 @Dev 注解用于必须在开发环境中执行的测试。如果需要,这使得以后更改环境名称变得容易,而无需修改所有测试类。

@Dev
@Test
void someTest() {
    // ...
}

5. 如何编写测试?

测试编写风格在 JUnit 4 和 JUnit 5 之间没有太大变化。

  • 测试类是包含至少一个测试方法的任何顶层类、static 成员类或 @Nested 类。测试类不能是 abstract 并且必须有一个构造函数。
  • 测试方法是使用 @Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate 注解编写的。
  • 测试类、测试方法和生命周期方法不需要是 public,但它们不能是 private。 建议使用 public 修饰符。

以下是一些带有生命周期方法的示例测试。请注意,所有注解都来自 org.junit.jupiter.api 包。

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import com.howtodoinjava.junit5.examples.Calculator;

public class AppTest {

  @BeforeAll
  static void setup(){
    System.out.println("@BeforeAll executed");
  }

  @BeforeEach
  void setupThis(){
    System.out.println("@BeforeEach executed");
  }

  @Tag("DEV")
  @Test
  void testCalcOne() {
    Assertions.assertEquals( 4 , Calculator.add(2, 2));
  }

  @Tag("PROD")
  @Disabled
  @Test
  void testCalcTwo(){
    Assertions.assertEquals( 6 , Calculator.add(2, 4));
  }

  @AfterEach
  void tearThis(){
    System.out.println("@AfterEach executed");
  }

  @AfterAll
  static void tear(){
    System.out.println("@AfterAll executed");
  }
}

6. 测试套件

使用 JUnit 5 测试套件,您可以运行分布在多个测试类和不同包中的测试。JUnit 5 提供这些注解来创建测试套件。

  • @Suite
  • @SelectClasses
  • @SelectPackages
  • @IncludePackages
  • @ExcludePackages
  • @IncludeClassNamePatterns
  • @ExcludeClassNamePatterns
  • @IncludeTags
  • @ExcludeTags

要执行套件,需要使用 @Suite 注解并在项目依赖项中包含 junit-platform-suite 模块。

@Suite
@SelectPackages("com.howtodoinjava.junit5.examples")
public class JUnit5TestSuiteExample {
   //...
}

7. 测试断言

断言有助于验证预期输出与测试的实际输出。

为了简单起见,所有 JUnit Jupiter 断言都是 org.junit.jupiter.Assertions 类的 static 方法,例如 assertEquals()assertNotEquals()

void testCase() {
    //Test will pass
    Assertions.assertNotEquals(3, Calculator.add(2, 2));

    //Test will fail
    Assertions.assertNotEquals(4, Calculator.add(2, 2), "Calculator.add(2, 2) test failed");

    //Test will fail
    Supplier<String> messageSupplier  = () -> "Calculator.add(2, 2) test failed";
    Assertions.assertNotEquals(4, Calculator.add(2, 2), messageSupplier);
}

更多信息:JUnit 5 断言

8. 测试假设

Assumptions 类提供 static 方法,以支持基于假设的条件测试执行。失败的假设会导致测试中止。

假设通常用于当继续执行给定的测试方法没有意义时。

Assumptions 类有三种方法:assumeFalse()assumeTrue()assumingThat()

public class AppTest {
    @Test
    void testOnDev()
    {
        System.setProperty("ENV", "DEV");
        Assumptions.assumeTrue("DEV".equals(System.getProperty("ENV")), AppTest::message);
    }

    @Test
    void testOnProd()
    {
        System.setProperty("ENV", "PROD");
        Assumptions.assumeFalse("DEV".equals(System.getProperty("ENV")));
    }

    private static String message () {
        return "TEST Execution Failed :: ";
    }
}

更多信息:JUnit 5 假设

9. JUnit 4 的向后兼容性

由于所有特定于 JUnit Jupiter 的类和注解都位于 org.junit.jupiter 基本包下,因此在类路径中同时使用 JUnit 4 和 JUnit Jupiter 不会导致任何冲突。因此,建议在新测试基础设施上编写新的测试。

JUnit 4 已经存在很长时间了,并且编写了大量的测试。JUnit Jupiter 也需要支持这些测试。为此,开发了 JUnit Vintage 子项目。

JUnit Vintage 提供了一种 TestEngine 实现,用于在 JUnit 5 平台上运行基于 JUnit 3 和 JUnit 4 的测试。只要类路径中存在‘junit-vintage-engine’工件,JUnit Platform 启动器将自动拾取 JUnit 3 和 JUnit 4 测试。

<dependencies>

	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>

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

</dependencies>

添加这些依赖项后,我们可以轻松地在 JUnit 5 环境中运行 JInit 4 测试。

请注意,如果项目具有所需的 Junit 5 依赖项(如本文开头所述),则可以在同一代码库中编写新的 JUnit 5 测试。

<dependencies>
	<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter-api</artifactId>
		<version>5.10.0</version>
		<scope>test</scope>
	</dependency>
		<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter-engine</artifactId>
		<version>5.10.0</version>
		<scope>test</scope>
	</dependency>

	<!-- JUnit 4 Vintage and JUnit 4 dependencies as well-->
</dependencies>

10. 从 JUnit 4 迁移到 JUnit 5

虽然 JUnit 5 支持 JUnit 4 的注解,但建议您迁移到新的注解,以充分利用 JUnit 5 的功能。

官方 JUnit 5 参考文档有一个 更改列表,我们需要进行这些更改才能进行稳健的迁移。它提供了一个迁移路径,借助 JUnit Vintage 测试引擎。主要更改如下

步骤讨论
替换/更新依赖项。JUnit 4 需要单个依赖项,而 JUnit 5 需要基于模块使用情况的几个依赖项。
替换注解JUnit 5 具有与 JUnit 4 不同的包结构,因此即使注解名称相同,我们也仍然需要更改 import 语句。
替换断言和假设JUnit 5 为断言和假设语句提供单独的类和包。我们需要使用新的类。
替换 JUnit 4 规则和运行程序这需要更仔细的更改。我们需要了解 JUnit 5 升级并在每个类中逐一进行这些更改。

如果您有大量依赖于 JUnit 4 的遗留代码或外部依赖项,您可能需要考虑更渐进的迁移策略。您可以先并行运行 JUnit 4 和 JUnit 5 测试,直到您确信可以完全迁移为止。

11. 结论

JUnit 5 感觉非常令人兴奋,功能也很丰富。现在,它也开放给第三方工具和 API 进行扩展。作为测试编写者,你可能不会觉得有太大区别,但当你尝试对其进行扩展或开发 IDE 插件时,你会赞叹它的设计。

你也可以考虑 将测试模板添加到 Eclipse IDE 中,以提高你的开发速度。

祝您学习愉快!!

源代码下载

发表评论

  1. 你好,你能解释一下如何在 Spring 中使用 JUnit 吗? 我有很多疑问。
    例如 1) 如何配置用于测试的应用程序上下文……还有更多

  2. 感谢你撰写这篇博客文章,它简短且切中要点!

    作为测试编写者,你可能不会觉得有太大区别,但当你尝试对其进行扩展或开发任何 IDE 插件时,你会赞叹它的设计。

    我必须说,查看 JUnit5 的源代码,我认为它比 JUnit4 复杂得多。为 JUnit5 编写扩展以改变测试方法执行方式似乎对我来说几乎不可能(而在 JUnit4 中这很容易!)

    我想能够在远程机器上运行每个测试方法。在 JUnit4 中,我会实现一个 Runner,并在 5 分钟内完成。在 JUnit5 中,看起来我必须实现一个完整的 TestEngine?! Extension 机制允许对测试实例进行后处理,但不能完全替换它(或者至少覆盖这些方法)。

    除了坚持使用 JUnit4 之外,你还有什么建议吗?

评论已关闭。

关于我们

HowToDoInJava 提供 Java 和相关技术的教程和操作指南。

它还分享最佳实践、算法和解决方案以及经常被问到的面试题。

我们的博客

REST API 教程

关注我们