1. 概述

本文档旨在为编写测试的程序员、扩展作者和引擎作者以及构建工具和 IDE 供应商提供全面的参考文档。

本文档也可以作为 PDF 下载 获取。

1.1. 什么是 JUnit 5?

与 JUnit 的先前版本不同,JUnit 5 由来自三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit 平台 + JUnit Jupiter + JUnit Vintage

JUnit 平台在 JVM 上启动测试框架 的基础。它还定义了 TestEngine API,用于开发在平台上运行的测试框架。此外,该平台还提供了一个 控制台启动器,用于从命令行启动平台,以及 JUnit 平台套件引擎,用于使用平台上的一个或多个测试引擎运行自定义测试套件。流行的 IDE(参见 IntelliJ IDEAEclipseNetBeansVisual Studio Code)和构建工具(参见 GradleMavenAnt)也对 JUnit 平台提供了头等支持。

JUnit Jupiter编程模型扩展模型 的组合,用于在 JUnit 5 中编写测试和扩展。Jupiter 子项目为在平台上运行基于 Jupiter 的测试提供了一个 TestEngine

JUnit Vintage 提供了一个 TestEngine,用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它需要在类路径或模块路径上存在 JUnit 4.12 或更高版本。

1.2. 支持的 Java 版本

JUnit 5 在运行时需要 Java 8(或更高版本)。但是,您仍然可以测试使用 JDK 的先前版本编译的代码。

1.3. 获取帮助

Stack Overflow 上询问与 JUnit 5 相关的问题,或在 Gitter 上与社区聊天。

1.4. 入门

1.4.1. 下载 JUnit 工件

要了解哪些工件可供下载并包含在您的项目中,请参阅 依赖项元数据。要为您的构建设置依赖项管理,请参阅 构建支持示例项目

1.4.2. JUnit 5 功能

要了解 JUnit 5 中有哪些功能以及如何使用它们,请阅读本用户指南中按主题组织的相应部分。

1.4.3. 示例项目

要查看您可以复制和试验的完整工作示例项目,junit5-samples 存储库是一个不错的起点。junit5-samples 存储库托管着一组基于 JUnit Jupiter、JUnit Vintage 和其他测试框架的示例项目。您将在示例项目中找到相应的构建脚本(例如,build.gradlepom.xml 等)。以下链接突出显示了一些您可以选择的组合。

2. 编写测试

以下示例简要介绍了在 JUnit Jupiter 中编写测试的最低要求。本章的后续部分将详细介绍所有可用功能。

第一个测试用例
import static org.junit.jupiter.api.Assertions.assertEquals;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class MyFirstJUnitJupiterTests {

    private final Calculator calculator = new Calculator();

    @Test
    void addition() {
        assertEquals(2, calculator.add(1, 1));
    }

}

2.1. 注解

JUnit Jupiter 支持以下注解来配置测试和扩展框架。

除非另有说明,所有核心注解都位于 org.junit.jupiter.api 包中,位于 junit-jupiter-api 模块中。

注解 描述

@Test

表示一个方法是测试方法。与 JUnit 4 的 @Test 注解不同,此注解不声明任何属性,因为 JUnit Jupiter 中的测试扩展基于它们自己的专用注解运行。此类方法是 继承 的,除非它们被 覆盖

@ParameterizedTest

表示一个方法是 参数化测试。此类方法是 继承 的,除非它们被 覆盖

@RepeatedTest

表示一个方法是 重复测试 的测试模板。此类方法是 继承 的,除非它们被 覆盖

@TestFactory

表示一个方法是 动态测试 的测试工厂。此类方法是 继承 的,除非它们被 覆盖

@TestTemplate

表示一个方法是 测试用例模板,旨在根据注册的 提供程序 返回的调用上下文数量多次调用。此类方法是 继承 的,除非它们被 覆盖

@TestClassOrder

用于配置注解测试类中 @Nested 测试类的 测试类执行顺序。此类注解是 继承 的。

@TestMethodOrder

用于配置注解测试类的 测试方法执行顺序;类似于 JUnit 4 的 @FixMethodOrder。此类注解是 继承 的。

@TestInstance

用于配置注解测试类的 测试实例生命周期。此类注解是 继承 的。

@DisplayName

为测试类或测试方法声明一个自定义的 显示名称。此类注解不是 继承 的。

@DisplayNameGeneration

为测试类声明一个自定义的 显示名称生成器。此类注解是 继承 的。

@BeforeEach

表示应在当前类中 每个 @Test@RepeatedTest@ParameterizedTest@TestFactory 方法 之前 执行注解方法;类似于 JUnit 4 的 @Before。此类方法是 继承 的 - 除非它们被 覆盖取代(即,仅基于签名替换,与 Java 的可见性规则无关)。

@AfterEach

表示应在当前类中 每个 @Test@RepeatedTest@ParameterizedTest@TestFactory 方法 之后 执行注解方法;类似于 JUnit 4 的 @After。此类方法是 继承 的 - 除非它们被 覆盖取代(即,仅基于签名替换,与 Java 的可见性规则无关)。

@BeforeAll

表示应在当前类中 所有 @Test@RepeatedTest@ParameterizedTest@TestFactory 方法 之前 执行注解方法;类似于 JUnit 4 的 @BeforeClass。此类方法是 继承 的 - 除非它们被 隐藏覆盖取代(即,仅基于签名替换,与 Java 的可见性规则无关) - 并且必须是 static,除非使用“每个类” 测试实例生命周期

@AfterAll

表示应在当前类中 所有 @Test@RepeatedTest@ParameterizedTest@TestFactory 方法 之后 执行注解方法;类似于 JUnit 4 的 @AfterClass。此类方法是 继承 的 - 除非它们被 隐藏覆盖取代(即,仅基于签名替换,与 Java 的可见性规则无关) - 并且必须是 static,除非使用“每个类” 测试实例生命周期

@Nested

表示注解类是 嵌套测试类。在 Java 8 到 Java 15 上,@BeforeAll@AfterAll 方法不能直接在 @Nested 测试类中使用,除非使用“每个类” 测试实例生命周期。从 Java 16 开始,@BeforeAll@AfterAll 方法可以在 @Nested 测试类中声明为 static,无论使用哪种测试实例生命周期模式。此类注解不是 继承 的。

@Tag

用于声明 用于过滤测试的标签,可以在类级别或方法级别使用;类似于 TestNG 中的测试组或 JUnit 4 中的类别。此类注解在类级别是 继承 的,但在方法级别不是。

@Disabled

用于 禁用 测试类或测试方法;类似于 JUnit 4 的 @Ignore。此类注解不是 继承 的。

@Timeout

用于在测试、测试工厂、测试模板或生命周期方法的执行超过给定持续时间时使其失败。此类注解是 继承 的。

@ExtendWith

用于 声明式注册扩展。此类注解是 继承 的。

@RegisterExtension

用于通过字段 以编程方式注册扩展。此类字段是 继承 的,除非它们被 遮蔽

@TempDir

用于通过字段注入或参数注入在生命周期方法或测试方法中提供 临时目录;位于 org.junit.jupiter.api.io 包中。

某些注解目前可能处于 实验性 状态。有关详细信息,请参阅 实验性 API 中的表格。

2.1.1. 元注解和组合注解

JUnit Jupiter 注解可以用作 元注解。这意味着您可以定义自己的 组合注解,该注解将自动 继承 其元注解的语义。

例如,与其在整个代码库中复制粘贴 @Tag("fast")(请参阅 标记和过滤),不如创建一个名为 @Fast 的自定义 组合注解。然后,@Fast 可以用作 @Tag("fast") 的直接替换。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.Tag;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}

以下 @Test 方法演示了 @Fast 注解的使用。

@Fast
@Test
void myFastTest() {
    // ...
}

您甚至可以更进一步,引入一个自定义的 @FastTest 注解,该注解可以用作 @Tag("fast") @Test 的直接替换。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}

JUnit 自动将以下内容识别为用“fast”标记的 @Test 方法。

@FastTest
void myFastTest() {
    // ...
}

2.2. 定义

平台概念
容器

测试树中的一个节点,它包含其他容器或测试作为其子节点(例如,测试类)。

测试

测试树中的一个节点,它在执行时验证预期行为(例如,@Test 方法)。

Jupiter 概念
生命周期方法

任何直接用 @BeforeAll@AfterAll@BeforeEach@AfterEach 注解或元注解的方法。

测试类

任何顶级类、static 成员类或 @Nested,它至少包含一个 测试方法,即一个 容器。测试类不能是 abstract 并且必须具有一个构造函数。

测试方法

任何直接用 @Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate 注解或元注解的实例方法。除了 @Test 之外,这些方法会在测试树中创建一个 容器,该容器对 测试 或(对于 @TestFactory 来说可能是)其他 容器 进行分组。

2.3. 测试类和方法

测试方法和生命周期方法可以在当前测试类中本地声明,从超类继承,或从接口继承(请参阅 测试接口和默认方法)。此外,测试方法和生命周期方法不能是 abstract 并且不能返回值(除了 @TestFactory 方法,它们需要返回值)。

类和方法可见性

测试类、测试方法和生命周期方法不需要是 public,但它们 不能private

通常建议省略测试类、测试方法和生命周期方法的 public 修饰符,除非有技术原因这样做 - 例如,当测试类被另一个包中的测试类扩展时。使类和方法成为 public 的另一个技术原因是在使用 Java 模块系统时简化模块路径上的测试。

以下测试类演示了 @Test 方法和所有支持的生命周期方法的使用。有关运行时语义的更多信息,请参阅 测试执行顺序回调的包装行为

标准测试类
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class StandardTests {

    @BeforeAll
    static void initAll() {
    }

    @BeforeEach
    void init() {
    }

    @Test
    void succeedingTest() {
    }

    @Test
    void failingTest() {
        fail("a failing test");
    }

    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
    }

    @Test
    void abortedTest() {
        assumeTrue("abc".contains("Z"));
        fail("test should have been aborted");
    }

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }

}

2.4. 显示名称

测试类和测试方法可以通过 @DisplayName 声明自定义显示名称 - 包含空格、特殊字符甚至表情符号 - 这些名称将在测试报告以及测试运行器和 IDE 中显示。

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("A special test case")
class DisplayNameDemo {

    @Test
    @DisplayName("Custom test name containing spaces")
    void testWithDisplayNameContainingSpaces() {
    }

    @Test
    @DisplayName("╯°□°)╯")
    void testWithDisplayNameContainingSpecialCharacters() {
    }

    @Test
    @DisplayName("😱")
    void testWithDisplayNameContainingEmoji() {
    }

}

2.4.1. 显示名称生成器

JUnit Jupiter 支持自定义显示名称生成器,可以通过 @DisplayNameGeneration 注解进行配置。通过 @DisplayName 注解提供的值始终优先于 DisplayNameGenerator 生成的显示名称。

生成器可以通过实现 DisplayNameGenerator 来创建。以下是一些 Jupiter 中可用的默认生成器

DisplayNameGenerator 行为

Standard

与自 JUnit Jupiter 5.0 发布以来的标准显示名称生成行为匹配。

Simple

为没有参数的方法删除尾随括号。

ReplaceUnderscores

将下划线替换为空格。

IndicativeSentences

通过连接测试名称和封闭类的名称来生成完整的句子。

请注意,对于 IndicativeSentences,您可以使用 @IndicativeSentencesGeneration 自定义分隔符和底层生成器,如下例所示。

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class DisplayNameGeneratorDemo {

    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class A_year_is_not_supported {

        @Test
        void if_it_is_zero() {
        }

        @DisplayName("A negative value for year is not supported by the leap year computation.")
        @ParameterizedTest(name = "For example, year {0} is not supported.")
        @ValueSource(ints = { -1, -4 })
        void if_it_is_negative(int year) {
        }

    }

    @Nested
    @IndicativeSentencesGeneration(separator = " -> ", generator = ReplaceUnderscores.class)
    class A_year_is_a_leap_year {

        @Test
        void if_it_is_divisible_by_4_but_not_by_100() {
        }

        @ParameterizedTest(name = "Year {0} is a leap year.")
        @ValueSource(ints = { 2016, 2020, 2048 })
        void if_it_is_one_of_the_following_years(int year) {
        }

    }

}
+-- DisplayNameGeneratorDemo [OK]
  +-- A year is not supported [OK]
  | +-- A negative value for year is not supported by the leap year computation. [OK]
  | | +-- For example, year -1 is not supported. [OK]
  | | '-- For example, year -4 is not supported. [OK]
  | '-- if it is zero() [OK]
  '-- A year is a leap year [OK]
    +-- A year is a leap year -> if it is divisible by 4 but not by 100. [OK]
    '-- A year is a leap year -> if it is one of the following years. [OK]
      +-- Year 2016 is a leap year. [OK]
      +-- Year 2020 is a leap year. [OK]
      '-- Year 2048 is a leap year. [OK]

2.4.2. 设置默认显示名称生成器

您可以使用 junit.jupiter.displayname.generator.default 配置参数 指定要默认使用的 DisplayNameGenerator 的完全限定类名。与通过 @DisplayNameGeneration 注释配置的显示名称生成器一样,提供的类必须实现 DisplayNameGenerator 接口。默认显示名称生成器将用于所有测试,除非封闭测试类或测试接口上存在 @DisplayNameGeneration 注释。通过 @DisplayName 注释提供的值始终优先于 DisplayNameGenerator 生成的显示名称。

例如,要默认使用 ReplaceUnderscores 显示名称生成器,您应该将配置参数设置为相应的完全限定类名(例如,在 src/test/resources/junit-platform.properties 中)

junit.jupiter.displayname.generator.default = \
    org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

同样,您可以指定实现 DisplayNameGenerator 的任何自定义类的完全限定名称。

总之,测试类或方法的显示名称根据以下优先级规则确定

  1. 如果存在,则为 @DisplayName 注释的值

  2. 如果存在,则通过调用 @DisplayNameGeneration 注释中指定的 DisplayNameGenerator

  3. 如果存在,则通过调用通过配置参数配置的默认 DisplayNameGenerator

  4. 通过调用 org.junit.jupiter.api.DisplayNameGenerator.Standard

2.5. 断言

JUnit Jupiter 附带了 JUnit 4 的许多断言方法,并添加了一些适合与 Java 8 lambda 一起使用的方法。所有 JUnit Jupiter 断言都是 org.junit.jupiter.api.Assertions 类中的 static 方法。

import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.CountDownLatch;

import example.domain.Person;
import example.util.Calculator;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

class AssertionsDemo {

    private final Calculator calculator = new Calculator();

    private final Person person = new Person("Jane", "Doe");

    @Test
    void standardAssertions() {
        assertEquals(2, calculator.add(1, 1));
        assertEquals(4, calculator.multiply(2, 2),
                "The optional failure message is now the last parameter");
        assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and all
        // failures will be reported together.
        assertAll("person",
            () -> assertEquals("Jane", person.getFirstName()),
            () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        // Within a code block, if an assertion fails the
        // subsequent code in the same block will be skipped.
        assertAll("properties",
            () -> {
                String firstName = person.getFirstName();
                assertNotNull(firstName);

                // Executed only if the previous assertion is valid.
                assertAll("first name",
                    () -> assertTrue(firstName.startsWith("J")),
                    () -> assertTrue(firstName.endsWith("e"))
                );
            },
            () -> {
                // Grouped assertion, so processed independently
                // of results of first name assertions.
                String lastName = person.getLastName();
                assertNotNull(lastName);

                // Executed only if the previous assertion is valid.
                assertAll("last name",
                    () -> assertTrue(lastName.startsWith("D")),
                    () -> assertTrue(lastName.endsWith("e"))
                );
            }
        );
    }

    @Test
    void exceptionTesting() {
        Exception exception = assertThrows(ArithmeticException.class, () ->
            calculator.divide(1, 0));
        assertEquals("/ by zero", exception.getMessage());
    }

    @Test
    void timeoutNotExceeded() {
        // The following assertion succeeds.
        assertTimeout(ofMinutes(2), () -> {
            // Perform task that takes less than 2 minutes.
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        // The following assertion succeeds, and returns the supplied object.
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod() {
        // The following assertion invokes a method reference and returns an object.
        String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
        assertEquals("Hello, World!", actualGreeting);
    }

    @Test
    void timeoutExceeded() {
        // The following assertion fails with an error message similar to:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            new CountDownLatch(1).await();
        });
    }

    private static String greeting() {
        return "Hello, World!";
    }

}
使用 assertTimeoutPreemptively() 进行抢占式超时

Assertions 类中的各种 assertTimeoutPreemptively() 方法在与调用代码不同的线程中执行提供的 executablesupplier。如果在 executablesupplier 中执行的代码依赖于 java.lang.ThreadLocal 存储,则此行为会导致不良副作用。

一个常见的例子是 Spring 框架中的事务测试支持。具体来说,Spring 的测试支持在调用测试方法之前将事务状态绑定到当前线程(通过 ThreadLocal)。因此,如果提供给 assertTimeoutPreemptively()executablesupplier 调用参与事务的 Spring 管理的组件,则这些组件采取的任何操作都不会与测试管理的事务一起回滚。相反,这些操作将提交到持久存储(例如,关系数据库),即使测试管理的事务被回滚。

依赖于 ThreadLocal 存储的其他框架也可能会遇到类似的副作用。

2.5.1. Kotlin 断言支持

JUnit Jupiter 还附带了一些断言方法,这些方法非常适合在 Kotlin 中使用。所有 JUnit Jupiter Kotlin 断言都是 org.junit.jupiter.api 包中的顶级函数。

import example.domain.Person
import example.util.Calculator
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.assertTimeout
import org.junit.jupiter.api.assertTimeoutPreemptively
import java.time.Duration

class KotlinAssertionsDemo {

    private val person = Person("Jane", "Doe")
    private val people = setOf(person, Person("John", "Doe"))

    @Test
    fun `exception absence testing`() {
        val calculator = Calculator()
        val result = assertDoesNotThrow("Should not throw an exception") {
            calculator.divide(0, 1)
        }
        assertEquals(0, result)
    }

    @Test
    fun `expected exception testing`() {
        val calculator = Calculator()
        val exception = assertThrows<ArithmeticException> ("Should throw an exception") {
            calculator.divide(1, 0)
        }
        assertEquals("/ by zero", exception.message)
    }

    @Test
    fun `grouped assertions`() {
        assertAll(
            "Person properties",
            { assertEquals("Jane", person.firstName) },
            { assertEquals("Doe", person.lastName) }
        )
    }

    @Test
    fun `grouped assertions from a stream`() {
        assertAll(
            "People with first name starting with J",
            people
                .stream()
                .map {
                    // This mapping returns Stream<() -> Unit>
                    { assertTrue(it.firstName.startsWith("J")) }
                }
        )
    }

    @Test
    fun `grouped assertions from a collection`() {
        assertAll(
            "People with last name of Doe",
            people.map { { assertEquals("Doe", it.lastName) } }
        )
    }

    @Test
    fun `timeout not exceeded testing`() {
        val fibonacciCalculator = FibonacciCalculator()
        val result = assertTimeout(Duration.ofMillis(1000)) {
            fibonacciCalculator.fib(14)
        }
        assertEquals(377, result)
    }

    @Test
    fun `timeout exceeded with preemptive termination`() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(Duration.ofMillis(10)) {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100)
        }
    }
}

2.5.2. 第三方断言库

尽管 JUnit Jupiter 提供的断言功能足以满足许多测试场景,但有时需要更强大的功能和额外功能,例如匹配器。在这种情况下,JUnit 团队建议使用第三方断言库,例如 AssertJHamcrestTruth 等。因此,开发人员可以自由使用他们选择的断言库。

例如,匹配器和流畅 API 的组合可用于使断言更具描述性和可读性。但是,JUnit Jupiter 的 org.junit.jupiter.api.Assertions 类不提供像 JUnit 4 的 org.junit.Assert 类中找到的 assertThat() 方法,该方法接受 Hamcrest Matcher。相反,鼓励开发人员使用第三方断言库提供的对匹配器的内置支持。

以下示例演示了如何在 JUnit Jupiter 测试中使用 Hamcrest 的 assertThat() 支持。只要 Hamcrest 库已添加到类路径中,您就可以静态导入 assertThat()is()equalTo() 等方法,然后在测试中使用它们,就像下面的 assertWithHamcrestMatcher() 方法一样。

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class HamcrestAssertionsDemo {

    private final Calculator calculator = new Calculator();

    @Test
    void assertWithHamcrestMatcher() {
        assertThat(calculator.subtract(4, 1), is(equalTo(3)));
    }

}

当然,基于 JUnit 4 编程模型的传统测试可以继续使用 org.junit.Assert#assertThat

2.6. 假设

JUnit Jupiter 附带了 JUnit 4 提供的假设方法的子集,并添加了一些适合与 Java 8 lambda 表达式和方法引用一起使用的方法。所有 JUnit Jupiter 假设都是 org.junit.jupiter.api.Assumptions 类中的静态方法。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class AssumptionsDemo {

    private final Calculator calculator = new Calculator();

    @Test
    void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));
        // remainder of test
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "Aborting test: not on developer workstation");
        // remainder of test
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // perform these assertions only on the CI server
                assertEquals(2, calculator.divide(4, 2));
            });

        // perform these assertions in all environments
        assertEquals(42, calculator.multiply(6, 7));
    }

}
从 JUnit Jupiter 5.4 开始,也可以使用 JUnit 4 的 org.junit.Assume 类中的方法进行假设。具体来说,JUnit Jupiter 支持 JUnit 4 的 AssumptionViolatedException 来表示应中止测试而不是将其标记为失败。

2.7. 禁用测试

可以通过 @Disabled 注释、通过 条件测试执行 中讨论的注释之一,或通过自定义 ExecutionCondition禁用整个测试类或单个测试方法。

这是一个 @Disabled 测试类。

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {

    @Test
    void testWillBeSkipped() {
    }

}

这是一个包含 @Disabled 测试方法的测试类。

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class DisabledTestsDemo {

    @Disabled("Disabled until bug #42 has been resolved")
    @Test
    void testWillBeSkipped() {
    }

    @Test
    void testWillBeExecuted() {
    }

}

@Disabled 可以不提供原因而声明;但是,JUnit 团队建议开发人员提供简短的解释说明为什么禁用了测试类或测试方法。因此,以上示例都展示了原因的使用方式,例如 @Disabled("Disabled until bug #42 has been resolved")。一些开发团队甚至要求在原因中提供问题跟踪编号,以实现自动跟踪等。

@Disabled 不是 @Inherited。因此,如果您希望禁用其超类为 @Disabled 的类,则必须在子类上重新声明 @Disabled

2.8. 条件测试执行

JUnit Jupiter 中的 ExecutionCondition 扩展 API 允许开发人员以编程方式根据某些条件启用禁用容器或测试。这种条件的最简单示例是内置的 DisabledCondition,它支持 @Disabled 注释(请参阅 禁用测试)。除了 @Disabled 之外,JUnit Jupiter 还支持 org.junit.jupiter.api.condition 包中的一些其他基于注释的条件,这些条件允许开发人员声明式地启用或禁用容器和测试。当注册多个 ExecutionCondition 扩展时,只要其中一个条件返回禁用,容器或测试就会被禁用。如果您希望提供有关它们可能被禁用的详细信息,与这些内置条件关联的每个注释都具有一个 disabledReason 属性可用于此目的。

有关详细信息,请参阅 ExecutionCondition 和以下部分。

组合注释

请注意,以下部分列出的任何条件注释也可以用作元注释,以创建自定义组合注释。例如,@EnabledOnOs 演示 中的 @TestOnMac 注释展示了如何将 @Test@EnabledOnOs 组合到一个可重用的注释中。

JUnit Jupiter 中的条件注释不是 @Inherited。因此,如果您希望对子类应用相同的语义,则必须在每个子类上重新声明每个条件注释。

除非另有说明,否则以下部分列出的每个条件注释只能在给定的测试接口、测试类或测试方法上声明一次。如果条件注释直接存在、间接存在或元存在于给定元素上多次,则 JUnit 将只使用发现的第一个这样的注释;任何额外的声明将被静默忽略。但是请注意,每个条件注释都可以与 org.junit.jupiter.api.condition 包中的其他条件注释一起使用。

2.8.1. 操作系统和体系结构条件

容器或测试可以通过 @EnabledOnOs@DisabledOnOs 注释在特定操作系统、体系结构或两者的组合上启用或禁用。

基于操作系统的条件执行
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
    // ...
}

@TestOnMac
void testOnMac() {
    // ...
}

@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
    // ...
}

@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
    // ...
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}
基于体系结构的条件执行
@Test
@EnabledOnOs(architectures = "aarch64")
void onAarch64() {
    // ...
}

@Test
@DisabledOnOs(architectures = "x86_64")
void notOnX86_64() {
    // ...
}

@Test
@EnabledOnOs(value = MAC, architectures = "aarch64")
void onNewMacs() {
    // ...
}

@Test
@DisabledOnOs(value = MAC, architectures = "aarch64")
void notOnNewMacs() {
    // ...
}

2.8.2. Java 运行时环境条件

容器或测试可以通过 @EnabledOnJre@DisabledOnJre 注释在 Java 运行时环境 (JRE) 的特定版本上启用或禁用,或者可以通过 @EnabledForJreRange@DisabledForJreRange 注释在 JRE 的特定版本范围内启用或禁用。范围默认为 JRE.JAVA_8 作为下边界 (min) 和 JRE.OTHER 作为上边界 (max),这允许使用半开范围。

@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
    // ...
}

@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
    // ...
}

@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
    // ...
}

@Test
@EnabledForJreRange(min = JAVA_9)
void fromJava9toCurrentJavaFeatureNumber() {
    // ...
}

@Test
@EnabledForJreRange(max = JAVA_11)
void fromJava8To11() {
    // ...
}

@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
    // ...
}

@Test
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
void notFromJava9to11() {
    // ...
}

@Test
@DisabledForJreRange(min = JAVA_9)
void notFromJava9toCurrentJavaFeatureNumber() {
    // ...
}

@Test
@DisabledForJreRange(max = JAVA_11)
void notFromJava8to11() {
    // ...
}

2.8.3. 原生映像条件

容器或测试可以通过 @EnabledInNativeImage@DisabledInNativeImage 注释在 GraalVM 原生映像 中启用或禁用。这些注释通常用于在使用 GraalVM 原生构建工具 项目中的 Gradle 和 Maven 插件在原生映像中运行测试时使用。

@Test
@EnabledInNativeImage
void onlyWithinNativeImage() {
    // ...
}

@Test
@DisabledInNativeImage
void neverWithinNativeImage() {
    // ...
}

2.8.4. 系统属性条件

根据named JVM 系统属性的值,可以使用@EnabledIfSystemProperty@DisabledIfSystemProperty 注解启用或禁用容器或测试。通过matches 属性提供的值将被解释为正则表达式。

@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
    // ...
}

@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
    // ...
}

从 JUnit Jupiter 5.6 开始,@EnabledIfSystemProperty@DisabledIfSystemProperty可重复注解。因此,这些注解可以在测试接口、测试类或测试方法上声明多次。具体来说,如果这些注解直接存在、间接存在或元存在于给定元素上,则会找到这些注解。

2.8.5. 环境变量条件

可以使用@EnabledIfEnvironmentVariable@DisabledIfEnvironmentVariable 注解,根据底层操作系统的named 环境变量的值启用或禁用容器或测试。通过matches 属性提供的值将被解释为正则表达式。

@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
    // ...
}

@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
    // ...
}

从 JUnit Jupiter 5.6 开始,@EnabledIfEnvironmentVariable@DisabledIfEnvironmentVariable可重复注解。因此,这些注解可以在测试接口、测试类或测试方法上声明多次。具体来说,如果这些注解直接存在、间接存在或元存在于给定元素上,则会找到这些注解。

2.8.6. 自定义条件

作为实现 ExecutionCondition 的替代方案,可以使用通过@EnabledIf@DisabledIf 注解配置的条件方法启用或禁用容器或测试。条件方法必须具有boolean 返回类型,并且可以不接受参数,也可以接受单个ExtensionContext 参数。

以下测试类演示了如何通过@EnabledIf@DisabledIf 配置名为customCondition 的本地方法。

@Test
@EnabledIf("customCondition")
void enabled() {
    // ...
}

@Test
@DisabledIf("customCondition")
void disabled() {
    // ...
}

boolean customCondition() {
    return true;
}

或者,条件方法可以位于测试类之外。在这种情况下,它必须通过其完全限定名称引用,如以下示例所示。

package example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;

class ExternalCustomConditionDemo {

    @Test
    @EnabledIf("example.ExternalCondition#customCondition")
    void enabled() {
        // ...
    }

}

class ExternalCondition {

    static boolean customCondition() {
        return true;
    }

}

在以下几种情况下,条件方法需要是static

  • @EnabledIf@DisabledIf 在类级别使用时

  • @EnabledIf@DisabledIf@ParameterizedTest@TestTemplate 方法上使用时

  • 当条件方法位于外部类中时

在任何其他情况下,可以使用静态方法或实例方法作为条件方法。

通常,可以使用实用程序类中的现有静态方法作为自定义条件。

例如,java.awt.GraphicsEnvironment 提供了一个public static boolean isHeadless() 方法,可用于确定当前环境是否不支持图形显示。因此,如果您的测试依赖于图形支持,则可以在图形支持不可用时将其禁用,如下所示。

@DisabledIf(value = "java.awt.GraphicsEnvironment#isHeadless",
    disabledReason = "headless environment")

2.9. 标记和过滤

可以使用@Tag 注解标记测试类和方法。这些标记稍后可用于过滤 测试发现和执行。有关 JUnit Platform 中标记支持的更多信息,请参阅 标记 部分。

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("fast")
@Tag("model")
class TaggingDemo {

    @Test
    @Tag("taxes")
    void testingTaxCalculation() {
    }

}
有关演示如何为标记创建自定义注解的示例,请参阅 元注解和组合注解

2.10. 测试执行顺序

默认情况下,测试类和方法将使用确定性但故意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行测试类和测试方法,从而允许可重复构建。

有关测试方法测试类的定义,请参阅 定义

2.10.1. 方法顺序

虽然真正的单元测试通常不应该依赖于执行的顺序,但在某些情况下需要强制执行特定的测试方法执行顺序,例如,在编写集成测试功能测试时,测试的顺序很重要,尤其是在与@TestInstance(Lifecycle.PER_CLASS) 结合使用时。

要控制测试方法的执行顺序,请使用@TestMethodOrder 注解您的测试类或测试接口,并指定所需的MethodOrderer 实现。您可以实现自己的自定义MethodOrderer 或使用以下内置MethodOrderer 实现之一。

另请参阅:回调的包装行为

以下示例演示了如何保证测试方法按通过@Order 注解指定的顺序执行。

import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {

    @Test
    @Order(1)
    void nullValues() {
        // perform assertions against null values
    }

    @Test
    @Order(2)
    void emptyValues() {
        // perform assertions against empty values
    }

    @Test
    @Order(3)
    void validValues() {
        // perform assertions against valid values
    }

}
设置默认方法排序器

您可以使用junit.jupiter.testmethod.order.default 配置参数 指定要默认使用的MethodOrderer 的完全限定类名。与通过@TestMethodOrder 注解配置的排序器一样,提供的类必须实现MethodOrderer 接口。默认排序器将用于所有测试,除非在封闭的测试类或测试接口上存在@TestMethodOrder 注解。

例如,要默认使用MethodOrderer.OrderAnnotation 方法排序器,您应该将配置参数设置为相应的完全限定类名(例如,在src/test/resources/junit-platform.properties 中)。

junit.jupiter.testmethod.order.default = \
    org.junit.jupiter.api.MethodOrderer$OrderAnnotation

同样,您可以指定实现MethodOrderer 的任何自定义类的完全限定名称。

2.10.2. 类顺序

虽然测试类通常不应该依赖于执行的顺序,但在某些情况下,需要强制执行特定的测试类执行顺序。您可能希望以随机顺序执行测试类,以确保测试类之间没有意外的依赖关系,或者您可能希望对测试类进行排序以优化构建时间,如以下场景中所述。

  • 首先运行之前失败的测试和更快的测试:“快速失败”模式

  • 在启用并行执行的情况下,首先安排较长的测试:“最短测试计划执行时间”模式

  • 各种其他用例

要为整个测试套件全局配置测试类执行顺序,请使用junit.jupiter.testclass.order.default 配置参数 指定要使用的ClassOrderer 的完全限定类名。提供的类必须实现ClassOrderer 接口。

您可以实现自己的自定义ClassOrderer 或使用以下内置ClassOrderer 实现之一。

例如,要使@Order 注解在测试类上生效,您应该使用配置参数和相应的完全限定类名(例如,在src/test/resources/junit-platform.properties 中)配置ClassOrderer.OrderAnnotation 类排序器。

junit.jupiter.testclass.order.default = \
    org.junit.jupiter.api.ClassOrderer$OrderAnnotation

配置的ClassOrderer 将应用于所有顶级测试类(包括static 嵌套测试类)和@Nested 测试类。

顶级测试类将相对于彼此进行排序;而@Nested 测试类将相对于共享相同封闭类的其他@Nested 测试类进行排序。

要为@Nested 测试类本地配置测试类执行顺序,请在要排序的@Nested 测试类的封闭类上声明@TestClassOrder 注解,并在@TestClassOrder 注解中直接提供要使用的ClassOrderer 实现的类引用。配置的ClassOrderer 将递归应用于@Nested 测试类及其@Nested 测试类。请注意,本地@TestClassOrder 声明始终会覆盖继承的@TestClassOrder 声明或通过junit.jupiter.testclass.order.default 配置参数全局配置的ClassOrderer

以下示例演示了如何保证@Nested 测试类按通过@Order 注解指定的顺序执行。

import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;

@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {

    @Nested
    @Order(1)
    class PrimaryTests {

        @Test
        void test1() {
        }
    }

    @Nested
    @Order(2)
    class SecondaryTests {

        @Test
        void test2() {
        }
    }
}

2.11. 测试实例生命周期

为了允许单独的测试方法以隔离方式执行,并避免由于可变测试实例状态而导致的意外副作用,JUnit 在执行每个测试方法(请参阅 定义)之前会创建每个测试类的新实例。这种“每方法”测试实例生命周期是 JUnit Jupiter 中的默认行为,与 JUnit 的所有先前版本类似。

请注意,即使在“每方法”测试实例生命周期模式处于活动状态时,如果给定的测试方法通过 条件(例如,@Disabled@DisabledOnOs 等)被禁用,测试类仍将被实例化。

如果您希望 JUnit Jupiter 在同一个测试实例上执行所有测试方法,请使用 @TestInstance(Lifecycle.PER_CLASS) 注解您的测试类。使用此模式时,将为每个测试类创建一个新的测试实例。因此,如果您的测试方法依赖于存储在实例变量中的状态,您可能需要在 @BeforeEach@AfterEach 方法中重置该状态。

“per-class” 模式比默认的“per-method” 模式有一些额外的优势。具体来说,使用“per-class” 模式,可以将 @BeforeAll@AfterAll 声明在非静态方法以及接口 default 方法上。因此,“per-class” 模式也使得在 @Nested 测试类中使用 @BeforeAll@AfterAll 方法成为可能。

从 Java 16 开始,@BeforeAll@AfterAll 方法可以在 @Nested 测试类中声明为 static

如果您使用 Kotlin 编程语言编写测试,您可能还会发现通过切换到“per-class” 测试实例生命周期模式,更容易实现非静态 @BeforeAll@AfterAll 生命周期方法以及 @MethodSource 工厂方法。

2.11.1. 更改默认测试实例生命周期

如果测试类或测试接口没有使用 @TestInstance 注解,JUnit Jupiter 将使用默认生命周期模式。标准默认模式是 PER_METHOD;但是,可以更改整个测试计划执行的默认模式。要更改默认测试实例生命周期模式,请将 junit.jupiter.testinstance.lifecycle.default 配置参数设置为 TestInstance.Lifecycle 中定义的枚举常量的名称,不区分大小写。这可以作为 JVM 系统属性提供,作为传递给 LauncherLauncherDiscoveryRequest 中的配置参数,或者通过 JUnit Platform 配置文件提供(有关详细信息,请参阅 配置参数)。

例如,要将默认测试实例生命周期模式设置为 Lifecycle.PER_CLASS,您可以使用以下系统属性启动 JVM。

-Djunit.jupiter.testinstance.lifecycle.default=per_class

但是请注意,通过 JUnit Platform 配置文件设置默认测试实例生命周期模式是一种更稳健的解决方案,因为配置文件可以与您的项目一起签入版本控制系统,因此可以在 IDE 和构建软件中使用。

要通过 JUnit Platform 配置文件将默认测试实例生命周期模式设置为 Lifecycle.PER_CLASS,请在类路径的根目录(例如,src/test/resources)中创建一个名为 junit-platform.properties 的文件,内容如下。

junit.jupiter.testinstance.lifecycle.default = per_class

如果更改默认测试实例生命周期模式,但未一致应用,则会导致不可预测的结果和脆弱的构建。例如,如果构建配置“per-class” 语义作为默认值,但在 IDE 中执行测试时使用“per-method” 语义,这可能会导致难以调试在构建服务器上发生的错误。因此,建议在 JUnit Platform 配置文件中更改默认值,而不是通过 JVM 系统属性更改默认值。

2.12. 嵌套测试

@Nested 测试为测试编写者提供了更多功能来表达几个测试组之间的关系。此类嵌套测试利用了 Java 的嵌套类,并促进了对测试结构的分层思考。以下是一个详细的示例,包括源代码和在 IDE 中执行的屏幕截图。

用于测试堆栈的嵌套测试套件
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.EmptyStackException;
import java.util.Stack;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

在 IDE 中执行此示例时,GUI 中的测试执行树将类似于以下图像。

writing tests nested test ide
在 IDE 中执行嵌套测试

在此示例中,通过为设置代码定义分层生命周期方法,外部测试中的先决条件在内部测试中使用。例如,createNewStack() 是一个 @BeforeEach 生命周期方法,它在定义它的测试类中使用,并在定义它的类下方嵌套树的所有级别中使用。

外部测试的设置代码在内部测试执行之前运行的事实使您能够独立运行所有测试。您甚至可以单独运行内部测试,而无需运行外部测试,因为外部测试的设置代码始终会执行。

只有非静态嵌套类(即内部类)可以作为 @Nested 测试类。嵌套可以是任意深度的,这些内部类都支持完整的生命周期支持,只有一个例外:@BeforeAll@AfterAll 方法默认不起作用。原因是 Java 在 Java 16 之前不允许内部类中的 static 成员。但是,可以通过使用 @TestInstance(Lifecycle.PER_CLASS) 注解 @Nested 测试类来规避此限制(请参阅 测试实例生命周期)。如果您使用的是 Java 16 或更高版本,则可以在 @Nested 测试类中将 @BeforeAll@AfterAll 方法声明为 static,并且此限制不再适用。

2.13. 构造函数和方法的依赖注入

在所有以前的 JUnit 版本中,测试构造函数或方法不允许具有参数(至少在标准 Runner 实现中不允许)。作为 JUnit Jupiter 的主要更改之一,测试构造函数和方法现在都允许具有参数。这允许更大的灵活性,并为构造函数和方法启用依赖注入

ParameterResolver 定义了希望在运行时动态解析参数的测试扩展的 API。如果测试类构造函数、测试方法生命周期方法(请参阅 定义)接受参数,则该参数必须在运行时由注册的 ParameterResolver 解析。

目前有三个内置解析器会自动注册。

  • TestInfoParameterResolver:如果构造函数或方法参数的类型为 TestInfo,则 TestInfoParameterResolver 将提供一个 TestInfo 实例,该实例对应于当前容器或测试,作为参数的值。然后可以使用 TestInfo 来检索有关当前容器或测试的信息,例如显示名称、测试类、测试方法和关联的标签。显示名称要么是技术名称(例如测试类或测试方法的名称),要么是通过 @DisplayName 配置的自定义名称。

    TestInfo 充当 JUnit 4 中 TestName 规则的直接替代。以下演示了如何将 TestInfo 注入测试构造函数、@BeforeEach 方法和 @Test 方法。

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

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;

@DisplayName("TestInfo Demo")
class TestInfoDemo {

    TestInfoDemo(TestInfo testInfo) {
        assertEquals("TestInfo Demo", testInfo.getDisplayName());
    }

    @BeforeEach
    void init(TestInfo testInfo) {
        String displayName = testInfo.getDisplayName();
        assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
    }

    @Test
    @DisplayName("TEST 1")
    @Tag("my-tag")
    void test1(TestInfo testInfo) {
        assertEquals("TEST 1", testInfo.getDisplayName());
        assertTrue(testInfo.getTags().contains("my-tag"));
    }

    @Test
    void test2() {
    }

}
  • RepetitionExtension:如果 @RepeatedTest@BeforeEach@AfterEach 方法中的方法参数的类型为 RepetitionInfo,则 RepetitionExtension 将提供一个 RepetitionInfo 实例。然后可以使用 RepetitionInfo 来检索有关当前重复、总重复次数、已失败的重复次数以及对应 @RepeatedTest 的失败阈值的信息。但是请注意,RepetitionExtension 不会在 @RepeatedTest 的上下文中之外注册。请参阅 重复测试示例

  • TestReporterParameterResolver:如果构造函数或方法参数的类型为 TestReporter,则 TestReporterParameterResolver 将提供一个 TestReporter 实例。TestReporter 可用于发布有关当前测试运行的附加数据。可以通过 TestExecutionListener 中的 reportingEntryPublished() 方法使用这些数据,从而允许在 IDE 中查看这些数据或将其包含在报告中。

    在 JUnit Jupiter 中,您应该使用 TestReporter,就像您以前在 JUnit 4 中向 stdoutstderr 打印信息一样。使用 @RunWith(JUnitPlatform.class) 将把所有报告的条目输出到 stdout。此外,一些 IDE 会将报告条目打印到 stdout 或在测试结果的用户界面中显示它们。

class TestReporterDemo {

    @Test
    void reportSingleValue(TestReporter testReporter) {
        testReporter.publishEntry("a status message");
    }

    @Test
    void reportKeyValuePair(TestReporter testReporter) {
        testReporter.publishEntry("a key", "a value");
    }

    @Test
    void reportMultipleKeyValuePairs(TestReporter testReporter) {
        Map<String, String> values = new HashMap<>();
        values.put("user name", "dk38");
        values.put("award year", "1974");

        testReporter.publishEntry(values);
    }

}
其他参数解析器必须通过 @ExtendWith 注册适当的 扩展 来显式启用。

查看 RandomParametersExtension 以了解自定义 ParameterResolver 的示例。虽然它并非旨在用于生产环境,但它演示了扩展模型和参数解析过程的简单性和表现力。MyRandomParametersTest 演示了如何将随机值注入 @Test 方法。

@ExtendWith(RandomParametersExtension.class)
class MyRandomParametersTest {

    @Test
    void injectsInteger(@Random int i, @Random int j) {
        assertNotEquals(i, j);
    }

    @Test
    void injectsDouble(@Random double d) {
        assertEquals(0.0, d, 1.0);
    }

}

对于实际用例,请查看 MockitoExtensionSpringExtension 的源代码。

当要注入的参数的类型是 ParameterResolver 的唯一条件时,您可以使用通用的 TypeBasedParameterResolver 基类。supportsParameters 方法在幕后实现,并支持参数化类型。

2.14. 测试接口和默认方法

JUnit Jupiter 允许在接口 default 方法上声明 @Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate@BeforeEach@AfterEach@BeforeAll@AfterAll 可以声明在测试接口中的 static 方法上,也可以声明在接口 default 方法上,前提是测试接口或测试类使用 @TestInstance(Lifecycle.PER_CLASS) 注解(请参阅 测试实例生命周期)。以下是一些示例。

@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());

    @BeforeAll
    default void beforeAllTests() {
        logger.info("Before all tests");
    }

    @AfterAll
    default void afterAllTests() {
        logger.info("After all tests");
    }

    @BeforeEach
    default void beforeEachTest(TestInfo testInfo) {
        logger.info(() -> String.format("About to execute [%s]",
            testInfo.getDisplayName()));
    }

    @AfterEach
    default void afterEachTest(TestInfo testInfo) {
        logger.info(() -> String.format("Finished executing [%s]",
            testInfo.getDisplayName()));
    }

}
interface TestInterfaceDynamicTestsDemo {

    @TestFactory
    default Stream<DynamicTest> dynamicTestsForPalindromes() {
        return Stream.of("racecar", "radar", "mom", "dad")
            .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
    }

}

@ExtendWith@Tag 可以声明在测试接口上,以便实现该接口的类自动继承其标签和扩展。请参阅 测试执行回调之前和之后,了解 TimingExtension 的源代码。

@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}

然后,您可以在测试类中实现这些测试接口,以使它们应用。

class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutionLogger, TestInterfaceDynamicTestsDemo {

    @Test
    void isEqualValue() {
        assertEquals(1, "a".length(), "is always equal");
    }

}

运行 TestInterfaceDemo 会产生类似于以下的输出

INFO  example.TestLifecycleLogger - Before all tests
INFO  example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()]
INFO  example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms.
INFO  example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()]
INFO  example.TestLifecycleLogger - About to execute [isEqualValue()]
INFO  example.TimingExtension - Method [isEqualValue] took 1 ms.
INFO  example.TestLifecycleLogger - Finished executing [isEqualValue()]
INFO  example.TestLifecycleLogger - After all tests

此功能的另一个可能的应用是为接口契约编写测试。例如,您可以为 Object.equalsComparable.compareTo 的实现如何表现编写测试,如下所示。

public interface Testable<T> {

    T createValue();

}
public interface EqualsContract<T> extends Testable<T> {

    T createNotEqualValue();

    @Test
    default void valueEqualsItself() {
        T value = createValue();
        assertEquals(value, value);
    }

    @Test
    default void valueDoesNotEqualNull() {
        T value = createValue();
        assertFalse(value.equals(null));
    }

    @Test
    default void valueDoesNotEqualDifferentValue() {
        T value = createValue();
        T differentValue = createNotEqualValue();
        assertNotEquals(value, differentValue);
        assertNotEquals(differentValue, value);
    }

}
public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {

    T createSmallerValue();

    @Test
    default void returnsZeroWhenComparedToItself() {
        T value = createValue();
        assertEquals(0, value.compareTo(value));
    }

    @Test
    default void returnsPositiveNumberWhenComparedToSmallerValue() {
        T value = createValue();
        T smallerValue = createSmallerValue();
        assertTrue(value.compareTo(smallerValue) > 0);
    }

    @Test
    default void returnsNegativeNumberWhenComparedToLargerValue() {
        T value = createValue();
        T smallerValue = createSmallerValue();
        assertTrue(smallerValue.compareTo(value) < 0);
    }

}

然后,您可以在测试类中实现这两个契约接口,从而继承相应的测试。当然,您必须实现抽象方法。

class StringTests implements ComparableContract<String>, EqualsContract<String> {

    @Override
    public String createValue() {
        return "banana";
    }

    @Override
    public String createSmallerValue() {
        return "apple"; // 'a' < 'b' in "banana"
    }

    @Override
    public String createNotEqualValue() {
        return "cherry";
    }

}
上面的测试仅仅是作为示例,因此并不完整。

2.15. 重复测试

JUnit Jupiter 提供了通过使用 `@RepeatedTest` 注解方法并指定所需的总重复次数来重复测试指定次数的功能。每次重复测试的调用都类似于执行一个普通的 `@Test` 方法,完全支持相同的生命周期回调和扩展。

以下示例演示了如何声明一个名为 `repeatedTest()` 的测试,该测试将自动重复 10 次。

@RepeatedTest(10)
void repeatedTest() {
    // ...
}

从 JUnit Jupiter 5.10 开始,`@RepeatedTest` 可以配置一个失败阈值,该阈值表示在超过该阈值后,剩余的重复将自动跳过。将 `failureThreshold` 属性设置为小于总重复次数的正数,以便在遇到指定次数的失败后跳过剩余重复的调用。

例如,如果您使用 `@RepeatedTest` 来重复调用一个您怀疑可能不稳定的测试,那么一次失败就足以证明该测试不稳定,并且没有必要调用剩余的重复。为了支持这种特定用例,请将 `failureThreshold` 设置为 `1`。您也可以根据您的用例将阈值设置为大于 1 的数字。

默认情况下,`failureThreshold` 属性设置为 `Integer.MAX_VALUE`,表示不会应用任何失败阈值,这实际上意味着无论任何重复是否失败,都将调用指定的重复次数。

如果 `@RepeatedTest` 方法的重复是并行执行的,则无法保证失败阈值。因此,建议在配置并行执行时,使用 `@Execution(SAME_THREAD)` 注解 `@RepeatedTest` 方法。有关更多详细信息,请参阅 并行执行

除了指定重复次数和失败阈值之外,还可以通过 `@RepeatedTest` 注解的 `name` 属性为每次重复配置一个自定义显示名称。此外,显示名称可以是静态文本和动态占位符组合的模式。目前支持以下占位符。

  • {displayName}: `@RepeatedTest` 方法的显示名称

  • {currentRepetition}: 当前重复次数

  • {totalRepetitions}: 总重复次数

给定重复的默认显示名称是根据以下模式生成的:"repetition {currentRepetition} of {totalRepetitions}"。因此,先前 `repeatedTest()` 示例中各个重复的显示名称将是:repetition 1 of 10repetition 2 of 10 等。如果您希望 `@RepeatedTest` 方法的显示名称包含在每次重复的名称中,您可以定义自己的自定义模式或使用预定义的 `RepeatedTest.LONG_DISPLAY_NAME` 模式。后者等于 "{displayName} :: repetition {currentRepetition} of {totalRepetitions}",这将导致各个重复的显示名称类似于 repeatedTest() :: repetition 1 of 10repeatedTest() :: repetition 2 of 10 等。

为了检索有关当前重复、总重复次数、已失败的重复次数和失败阈值的信息,开发人员可以选择将 `RepetitionInfo` 实例注入到 `@RepeatedTest`、`@BeforeEach` 或 `@AfterEach` 方法中。

2.15.1. 重复测试示例

本节末尾的 `RepeatedTestsDemo` 类演示了几个重复测试的示例。

repeatedTest() 方法与上一节中的示例相同;而 `repeatedTestWithRepetitionInfo()` 演示了如何将 `RepetitionInfo` 实例注入测试以访问当前重复测试的总重复次数。

repeatedTestWithFailureThreshold() 演示了如何设置失败阈值,并模拟了每隔一次重复的意外失败。最终的行为可以在本节末尾的 `ConsoleLauncher` 输出中查看。

接下来的两个方法演示了如何在每次重复的显示名称中包含 `@RepeatedTest` 方法的自定义 `@DisplayName`。`customDisplayName()` 将自定义显示名称与自定义模式组合在一起,然后使用 `TestInfo` 验证生成的显示名称的格式。`Repeat!` 是来自 `@DisplayName` 声明的 `{displayName}`,而 `1/1` 来自 `{currentRepetition}/{totalRepetitions}`。相反,`customDisplayNameWithLongPattern()` 使用了前面提到的预定义 `RepeatedTest.LONG_DISPLAY_NAME` 模式。

repeatedTestInGerman() 演示了将重复测试的显示名称翻译成外语(在本例中为德语)的能力,从而导致各个重复的名称类似于:Wiederholung 1 von 5Wiederholung 2 von 5 等。

由于 `beforeEach()` 方法使用 `@BeforeEach` 注解,因此它将在每个重复测试的每次重复之前执行。通过将 `TestInfo` 和 `RepetitionInfo` 注入方法,我们看到可以获取有关当前正在执行的重复测试的信息。使用启用了 `INFO` 日志级别的 `RepeatedTestsDemo` 执行将导致以下输出。

INFO: About to execute repetition 1 of 10 for repeatedTest
INFO: About to execute repetition 2 of 10 for repeatedTest
INFO: About to execute repetition 3 of 10 for repeatedTest
INFO: About to execute repetition 4 of 10 for repeatedTest
INFO: About to execute repetition 5 of 10 for repeatedTest
INFO: About to execute repetition 6 of 10 for repeatedTest
INFO: About to execute repetition 7 of 10 for repeatedTest
INFO: About to execute repetition 8 of 10 for repeatedTest
INFO: About to execute repetition 9 of 10 for repeatedTest
INFO: About to execute repetition 10 of 10 for repeatedTest
INFO: About to execute repetition 1 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 2 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 3 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 4 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 5 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 1 of 8 for repeatedTestWithFailureThreshold
INFO: About to execute repetition 2 of 8 for repeatedTestWithFailureThreshold
INFO: About to execute repetition 3 of 8 for repeatedTestWithFailureThreshold
INFO: About to execute repetition 4 of 8 for repeatedTestWithFailureThreshold
INFO: About to execute repetition 1 of 1 for customDisplayName
INFO: About to execute repetition 1 of 1 for customDisplayNameWithLongPattern
INFO: About to execute repetition 1 of 5 for repeatedTestInGerman
INFO: About to execute repetition 2 of 5 for repeatedTestInGerman
INFO: About to execute repetition 3 of 5 for repeatedTestInGerman
INFO: About to execute repetition 4 of 5 for repeatedTestInGerman
INFO: About to execute repetition 5 of 5 for repeatedTestInGerman
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.logging.Logger;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;

class RepeatedTestsDemo {

    private Logger logger = // ...

    @BeforeEach
    void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        int currentRepetition = repetitionInfo.getCurrentRepetition();
        int totalRepetitions = repetitionInfo.getTotalRepetitions();
        String methodName = testInfo.getTestMethod().get().getName();
        logger.info(String.format("About to execute repetition %d of %d for %s", //
            currentRepetition, totalRepetitions, methodName));
    }

    @RepeatedTest(10)
    void repeatedTest() {
        // ...
    }

    @RepeatedTest(5)
    void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
        assertEquals(5, repetitionInfo.getTotalRepetitions());
    }

    @RepeatedTest(value = 8, failureThreshold = 2)
    void repeatedTestWithFailureThreshold(RepetitionInfo repetitionInfo) {
        // Simulate unexpected failure every second repetition
        if (repetitionInfo.getCurrentRepetition() % 2 == 0) {
            fail("Boom!");
        }
    }

    @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
    @DisplayName("Repeat!")
    void customDisplayName(TestInfo testInfo) {
        assertEquals("Repeat! 1/1", testInfo.getDisplayName());
    }

    @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
    @DisplayName("Details...")
    void customDisplayNameWithLongPattern(TestInfo testInfo) {
        assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
    }

    @RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
    void repeatedTestInGerman() {
        // ...
    }

}

当使用启用了 Unicode 主题的 `ConsoleLauncher` 时,执行 `RepeatedTestsDemo` 将导致以下输出到控制台。

├─ RepeatedTestsDemo ✔
│  ├─ repeatedTest() ✔
│  │  ├─ repetition 1 of 10 ✔
│  │  ├─ repetition 2 of 10 ✔
│  │  ├─ repetition 3 of 10 ✔
│  │  ├─ repetition 4 of 10 ✔
│  │  ├─ repetition 5 of 10 ✔
│  │  ├─ repetition 6 of 10 ✔
│  │  ├─ repetition 7 of 10 ✔
│  │  ├─ repetition 8 of 10 ✔
│  │  ├─ repetition 9 of 10 ✔
│  │  └─ repetition 10 of 10 ✔
│  ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔
│  │  ├─ repetition 1 of 5 ✔
│  │  ├─ repetition 2 of 5 ✔
│  │  ├─ repetition 3 of 5 ✔
│  │  ├─ repetition 4 of 5 ✔
│  │  └─ repetition 5 of 5 ✔
│  ├─ repeatedTestWithFailureThreshold(RepetitionInfo) ✔
│  │  ├─ repetition 1 of 8 ✔
│  │  ├─ repetition 2 of 8 ✘ Boom!
│  │  ├─ repetition 3 of 8 ✔
│  │  ├─ repetition 4 of 8 ✘ Boom!
│  │  ├─ repetition 5 of 8 ↷ Failure threshold [2] exceeded
│  │  ├─ repetition 6 of 8 ↷ Failure threshold [2] exceeded
│  │  ├─ repetition 7 of 8 ↷ Failure threshold [2] exceeded
│  │  └─ repetition 8 of 8 ↷ Failure threshold [2] exceeded
│  ├─ Repeat! ✔
│  │  └─ Repeat! 1/1 ✔
│  ├─ Details... ✔
│  │  └─ Details... :: repetition 1 of 1 ✔
│  └─ repeatedTestInGerman() ✔
│     ├─ Wiederholung 1 von 5 ✔
│     ├─ Wiederholung 2 von 5 ✔
│     ├─ Wiederholung 3 von 5 ✔
│     ├─ Wiederholung 4 von 5 ✔
│     └─ Wiederholung 5 von 5 ✔

2.16. 参数化测试

参数化测试使您可以使用不同的参数多次运行测试。它们的声明方式与普通的 `@Test` 方法相同,但使用 `@ParameterizedTest` 注解代替。此外,您必须声明至少一个,该将为每次调用提供参数,然后在测试方法中使用这些参数。

以下示例演示了一个参数化测试,该测试使用 `@ValueSource` 注解来指定 `String` 数组作为参数源。

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

执行上述参数化测试方法时,每次调用都将单独报告。例如,`ConsoleLauncher` 将打印类似于以下内容的输出。

palindromes(String) ✔
├─ [1] candidate=racecar ✔
├─ [2] candidate=radar ✔
└─ [3] candidate=able was I ere I saw elba ✔

2.16.1. 必需设置

为了使用参数化测试,您需要添加对 `junit-jupiter-params` 工件的依赖。有关详细信息,请参阅 依赖项元数据

2.16.2. 使用参数

参数化测试方法通常从配置的源(请参阅 参数源)中直接使用参数,遵循参数源索引和方法参数索引之间的一对一对应关系(请参阅 @CsvSource 中的示例)。但是,参数化测试方法也可以选择将来自源的参数聚合到传递给方法的单个对象中(请参阅 参数聚合)。其他参数也可以由 `ParameterResolver` 提供(例如,获取 `TestInfo`、`TestReporter` 等的实例)。具体来说,参数化测试方法必须根据以下规则声明形式参数。

  • 必须首先声明零个或多个索引参数

  • 接下来必须声明零个或多个聚合器

  • 最后必须声明零个或多个由 `ParameterResolver` 提供的参数。

在此上下文中,索引参数是指由 `ArgumentsProvider` 提供的 `Arguments` 中给定索引的参数,该参数作为参数传递给参数化方法,位于方法形式参数列表中的相同索引处。聚合器是任何类型为 `ArgumentsAccessor` 的参数,或任何使用 `@AggregateWith` 注解的参数。

可关闭参数

实现 `java.lang.AutoCloseable`(或扩展 `java.lang.AutoCloseable` 的 `java.io.Closeable`)的参数将在 `@AfterEach` 方法和 `AfterEachCallback` 扩展调用当前参数化测试调用的 `@AfterEach` 方法和 `AfterEachCallback` 扩展后自动关闭。

要阻止这种情况发生,请将 `@ParameterizedTest` 中的 `autoCloseArguments` 属性设置为 `false`。具体来说,如果实现 `AutoCloseable` 的参数被重复用于同一参数化测试方法的多次调用,则必须使用 `@ParameterizedTest(autoCloseArguments = false)` 注解该方法,以确保该参数不会在调用之间关闭。

2.16.3. 参数源

开箱即用,JUnit Jupiter 提供了许多注解。以下每个小节都简要概述了每个注解并提供了一个示例。有关更多信息,请参阅 `org.junit.jupiter.params.provider` 包中的 Javadoc。

@ValueSource

@ValueSource 是最简单的源之一。它允许您指定单个文字值数组,并且只能用于为每次参数化测试调用提供单个参数。

@ValueSource 支持以下类型的文字值。

  • short

  • byte

  • int

  • long

  • float

  • double

  • char

  • boolean

  • java.lang.String

  • java.lang.Class

例如,以下 `@ParameterizedTest` 方法将被调用三次,分别使用值 `1`、`2` 和 `3`。

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertTrue(argument > 0 && argument < 4);
}
空源和空源

为了检查极端情况并验证我们的软件在提供错误输入时是否表现正确,将 `null` 和值提供给我们的参数化测试可能很有用。以下注解用作接受单个参数的参数化测试的 `null` 和空值的源。

  • @NullSource: 为带注解的 `@ParameterizedTest` 方法提供单个 `null` 参数。

    • @NullSource 不能用于具有基本类型参数的参数。

  • @EmptySource: 为带注解的 `@ParameterizedTest` 方法提供单个参数,用于以下类型的参数:`java.lang.String`、`java.util.Collection`(以及具有 `public` 无参数构造函数的具体子类型)、`java.util.List`、`java.util.Set`、`java.util.SortedSet`、`java.util.NavigableSet`、`java.util.Map`(以及具有 `public` 无参数构造函数的具体子类型)、`java.util.SortedMap`、`java.util.NavigableMap`、基本数组(例如,`int[]`、`char[][]` 等)、对象数组(例如,`String[]`、`Integer[][]` 等)。

  • @NullAndEmptySource: 一个组合注解,它结合了 `@NullSource` 和 `@EmptySource` 的功能。

如果您需要为参数化测试提供多种类型的空白字符串,可以使用 @ValueSource 来实现这一点,例如,@ValueSource(strings = {" ", "   ", "\t", "\n"})

您还可以将 `@NullSource`、`@EmptySource` 和 `@ValueSource` 组合在一起,以测试更广泛的 `null`、空白输入。以下示例演示了如何为字符串实现这一点。

@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", "   ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
    assertTrue(text == null || text.trim().isEmpty());
}

使用组合的 `@NullAndEmptySource` 注解可以简化上述内容,如下所示。

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", "   ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
    assertTrue(text == null || text.trim().isEmpty());
}
`nullEmptyAndBlankStrings(String)` 参数化测试方法的两种变体都将导致六次调用:1 次用于 `null`,1 次用于空字符串,以及 4 次用于通过 `@ValueSource` 提供的显式空白字符串。
@EnumSource

@EnumSource 提供了一种方便的方式来使用 Enum 常量。

@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
    assertNotNull(unit);
}

注释的 value 属性是可选的。如果省略,则使用第一个方法参数的声明类型。如果它没有引用枚举类型,则测试将失败。因此,在上面的示例中,value 属性是必需的,因为方法参数被声明为 TemporalUnit,即 ChronoUnit 实现的接口,它不是枚举类型。将方法参数类型更改为 ChronoUnit 允许您从注释中省略显式枚举类型,如下所示。

@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
    assertNotNull(unit);
}

该注释提供了一个可选的 names 属性,它允许您指定要使用哪些常量,如以下示例所示。如果省略,将使用所有常量。

@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
    assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}

@EnumSource 注释还提供了一个可选的 mode 属性,它允许您对传递给测试方法的常量进行细粒度控制。例如,您可以从枚举常量池中排除名称或指定正则表达式,如以下示例所示。

@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
    assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
    assertTrue(unit.name().endsWith("DAYS"));
}
@MethodSource

@MethodSource 允许您引用测试类或外部类的工厂方法。

测试类中的工厂方法必须是 static,除非测试类用 @TestInstance(Lifecycle.PER_CLASS) 进行注释;而外部类中的工厂方法必须始终是 static

每个工厂方法都必须生成一个参数,并且流中的每组参数都将作为带注释的 @ParameterizedTest 方法的单个调用的实际参数提供。一般来说,这转化为一个 ArgumentsStream(即 Stream<Arguments>);但是,实际的具体返回类型可以采用多种形式。在此上下文中,“流”是指 JUnit 可以可靠地转换为 Stream 的任何东西,例如 StreamDoubleStreamLongStreamIntStreamCollectionIteratorIterable、对象数组或基本类型数组。“流”中的“参数”可以作为 Arguments 的实例、对象数组(例如 Object[])或单个值提供,如果参数化测试方法接受单个参数。

如果您只需要一个参数,您可以返回参数类型的实例的 Stream,如以下示例所示。

@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> stringProvider() {
    return Stream.of("apple", "banana");
}

如果您没有通过 @MethodSource 显式提供工厂方法名称,JUnit Jupiter 将根据惯例搜索与当前 @ParameterizedTest 方法同名的工厂方法。这在以下示例中得到证明。

@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> testWithDefaultLocalMethodSource() {
    return Stream.of("apple", "banana");
}

基本类型(DoubleStreamIntStreamLongStream)的流也受支持,如以下示例所示。

@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
    assertNotEquals(9, argument);
}

static IntStream range() {
    return IntStream.range(0, 20).skip(10);
}

如果参数化测试方法声明多个参数,您需要返回一个 Arguments 实例或对象数组的集合、流或数组,如下所示(有关支持的返回类型的更多详细信息,请参阅 @MethodSource 的 Javadoc)。请注意,arguments(Object…​) 是在 Arguments 接口中定义的静态工厂方法。此外,Arguments.of(Object…​) 可以用作 arguments(Object…​) 的替代方法。

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
    assertEquals(5, str.length());
    assertTrue(num >=1 && num <=2);
    assertEquals(2, list.size());
}

static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
        arguments("apple", 1, Arrays.asList("a", "b")),
        arguments("lemon", 2, Arrays.asList("x", "y"))
    );
}

可以通过提供其完全限定的方法名来引用外部的 static 工厂方法,如以下示例所示。

package example;

import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class ExternalMethodSourceDemo {

    @ParameterizedTest
    @MethodSource("example.StringsProviders#tinyStrings")
    void testWithExternalMethodSource(String tinyString) {
        // test with tiny string
    }
}

class StringsProviders {

    static Stream<String> tinyStrings() {
        return Stream.of(".", "oo", "OOO");
    }
}

工厂方法可以声明参数,这些参数将由注册的 ParameterResolver 扩展 API 实现提供。在以下示例中,工厂方法通过其名称引用,因为测试类中只有一个这样的方法。如果有多个具有相同名称的本地方法,也可以提供参数来区分它们 - 例如,@MethodSource("factoryMethod()")@MethodSource("factoryMethod(java.lang.String)")。或者,可以通过其完全限定的方法名引用工厂方法,例如 @MethodSource("example.MyTests#factoryMethod(java.lang.String)")

@RegisterExtension
static final IntegerResolver integerResolver = new IntegerResolver();

@ParameterizedTest
@MethodSource("factoryMethodWithArguments")
void testWithFactoryMethodWithArguments(String argument) {
    assertTrue(argument.startsWith("2"));
}

static Stream<Arguments> factoryMethodWithArguments(int quantity) {
    return Stream.of(
            arguments(quantity + " apples"),
            arguments(quantity + " lemons")
    );
}

static class IntegerResolver implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
            ExtensionContext extensionContext) {

        return parameterContext.getParameter().getType() == int.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext,
            ExtensionContext extensionContext) {

        return 2;
    }

}
@CsvSource

@CsvSource 允许您将参数列表表示为逗号分隔值(即 CSV String 文字)。通过 @CsvSource 中的 value 属性提供的每个字符串都代表一个 CSV 记录,并导致参数化测试调用一次。第一个记录可以选择用于提供 CSV 标题(有关详细信息和示例,请参阅 useHeadersInDisplayName 属性的 Javadoc)。

@ParameterizedTest
@CsvSource({
    "apple,         1",
    "banana,        2",
    "'lemon, lime', 0xF1",
    "strawberry,    700_000"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertNotEquals(0, rank);
}

默认分隔符是逗号 (,),但您可以通过设置 delimiter 属性来使用另一个字符。或者,delimiterString 属性允许您使用 String 分隔符而不是单个字符。但是,两个分隔符属性不能同时设置。

默认情况下,@CsvSource 使用单引号 (') 作为其引号字符,但这可以通过 quoteCharacter 属性更改。请参阅上面示例和下表中的 'lemon, lime' 值。一个空的、带引号的值 ('') 会导致一个空的 String,除非设置了 emptyValue 属性;而一个完全空的值将被解释为 null 引用。通过指定一个或多个 nullValues,可以将自定义值解释为 null 引用(请参阅表中的 NIL 示例)。如果 null 引用的目标类型是基本类型,则会抛出 ArgumentConversionException

未加引号的空值将始终转换为 null 引用,无论通过 nullValues 属性配置了哪些自定义值。

默认情况下,除了在带引号的字符串中,CSV 列中的前导和尾随空格都会被修剪。可以通过将 ignoreLeadingAndTrailingWhitespace 属性设置为 true 来更改此行为。

示例输入 结果参数列表

@CsvSource({ "apple, banana" })

"apple", "banana"

@CsvSource({ "apple, 'lemon, lime'" })

"apple", "lemon, lime"

@CsvSource({ "apple, ''" })

"apple", ""

@CsvSource({ "apple, " })

"apple", null

@CsvSource(value = { "apple, banana, NIL" }, nullValues = "NIL")

"apple", "banana", null

@CsvSource(value = { " apple , banana" }, ignoreLeadingAndTrailingWhitespace = false)

" apple ", " banana"

如果您的编程语言支持文本块(例如,Java SE 15 或更高版本),您可以选择使用 @CsvSourcetextBlock 属性。文本块中的每个记录都代表一个 CSV 记录,并导致参数化测试调用一次。通过将 useHeadersInDisplayName 属性设置为 true,第一个记录可以选择用于提供 CSV 标题,如以下示例所示。

使用文本块,前面的示例可以实现如下。

@ParameterizedTest(name = "[{index}] {arguments}")
@CsvSource(useHeadersInDisplayName = true, textBlock = """
    FRUIT,         RANK
    apple,         1
    banana,        2
    'lemon, lime', 0xF1
    strawberry,    700_000
    """)
void testWithCsvSource(String fruit, int rank) {
    // ...
}

为前面的示例生成的显示名称包括 CSV 标题名称。

[1] FRUIT = apple, RANK = 1
[2] FRUIT = banana, RANK = 2
[3] FRUIT = lemon, lime, RANK = 0xF1
[4] FRUIT = strawberry, RANK = 700_000

与通过 value 属性提供的 CSV 记录相反,文本块可以包含注释。任何以 # 符号开头的行都将被视为注释并被忽略。但是请注意,# 符号必须是行上的第一个字符,没有任何前导空格。因此,建议将结束文本块分隔符 (""") 放在输入的最后一行末尾或下一行,与输入的其余部分左对齐(如以下示例所示,该示例演示了类似于表格的格式)。

@ParameterizedTest
@CsvSource(delimiter = '|', quoteCharacter = '"', textBlock = """
    #-----------------------------
    #    FRUIT     |     RANK
    #-----------------------------
         apple     |      1
    #-----------------------------
         banana    |      2
    #-----------------------------
      "lemon lime" |     0xF1
    #-----------------------------
       strawberry  |    700_000
    #-----------------------------
    """)
void testWithCsvSource(String fruit, int rank) {
    // ...
}

Java 的 文本块 功能在代码编译时会自动删除偶然的空格。但是,其他 JVM 语言(如 Groovy 和 Kotlin)则不会。因此,如果您使用的是 Java 以外的编程语言,并且您的文本块在带引号的字符串中包含注释或换行符,则需要确保文本块中没有前导空格。

@CsvFileSource

@CsvFileSource 允许您从类路径或本地文件系统中使用逗号分隔值 (CSV) 文件。来自 CSV 文件的每个记录都会导致参数化测试调用一次。第一个记录可以选择用于提供 CSV 标题。您可以指示 JUnit 通过 numLinesToSkip 属性忽略标题。如果您希望在显示名称中使用标题,可以将 useHeadersInDisplayName 属性设置为 true。以下示例演示了 numLinesToSkipuseHeadersInDisplayName 的用法。

默认分隔符是逗号 (,),但您可以通过设置 delimiter 属性来使用另一个字符。或者,delimiterString 属性允许您使用 String 分隔符而不是单个字符。但是,两个分隔符属性不能同时设置。

CSV 文件中的注释
任何以 # 符号开头的行都将被解释为注释,并将被忽略。
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}

@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}

@ParameterizedTest(name = "[{index}] {arguments}")
@CsvFileSource(resources = "/two-column.csv", useHeadersInDisplayName = true)
void testWithCsvFileSourceAndHeaders(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}
two-column.csv
COUNTRY, REFERENCE
Sweden, 1
Poland, 2
"United States of America", 3
France, 700_000

以下列表显示了上面前两个参数化测试方法的生成的显示名称。

[1] country=Sweden, reference=1
[2] country=Poland, reference=2
[3] country=United States of America, reference=3
[4] country=France, reference=700_000

以下列表显示了上面最后一个使用 CSV 标题名称的参数化测试方法的生成的显示名称。

[1] COUNTRY = Sweden, REFERENCE = 1
[2] COUNTRY = Poland, REFERENCE = 2
[3] COUNTRY = United States of America, REFERENCE = 3
[4] COUNTRY = France, REFERENCE = 700_000

@CsvSource 中使用的默认语法相反,@CsvFileSource 默认情况下使用双引号 (") 作为引号字符,但这可以通过 quoteCharacter 属性更改。请参阅上面示例中的 "United States of America" 值。一个空的、带引号的值 ("") 会导致一个空的 String,除非设置了 emptyValue 属性;而一个完全空的值将被解释为 null 引用。通过指定一个或多个 nullValues,可以将自定义值解释为 null 引用。如果 null 引用的目标类型是基本类型,则会抛出 ArgumentConversionException

未加引号的空值将始终转换为 null 引用,无论通过 nullValues 属性配置了哪些自定义值。

默认情况下,除了在带引号的字符串中,CSV 列中的前导和尾随空格都会被修剪。可以通过将 ignoreLeadingAndTrailingWhitespace 属性设置为 true 来更改此行为。

@ArgumentsSource

@ArgumentsSource 可用于指定自定义的可重用 ArgumentsProvider。请注意,ArgumentsProvider 的实现必须声明为顶级类或 static 嵌套类。

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of("apple", "banana").map(Arguments::of);
    }
}

如果您希望实现一个也使用注释的自定义 ArgumentsProvider(如内置提供程序,例如 ValueArgumentsProviderCsvArgumentsProvider),您可以选择扩展 AnnotationBasedArgumentsProvider 类。

2.16.4. 参数转换

扩展转换

JUnit Jupiter 支持 扩展基本类型转换,用于提供给 @ParameterizedTest 的参数。例如,用 @ValueSource(ints = { 1, 2, 3 }) 注释的参数化测试可以声明为不仅接受 int 类型的参数,还接受 longfloatdouble 类型的参数。

隐式转换

为了支持像 @CsvSource 这样的用例,JUnit Jupiter 提供了一些内置的隐式类型转换器。转换过程取决于每个方法参数的声明类型。

例如,如果一个 @ParameterizedTest 声明了一个类型为 TimeUnit 的参数,而声明的源提供的实际类型是一个 String,那么该字符串将自动转换为相应的 TimeUnit 枚举常量。

@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit argument) {
    assertNotNull(argument.name());
}

String 实例会隐式转换为以下目标类型。

十进制、十六进制和八进制 String 字面量将被转换为它们的整型类型:byteshortintlong 以及它们的包装类型。
目标类型 示例

boolean/Boolean

"true"true (仅接受值 'true' 或 'false',不区分大小写)

byte/Byte

"15""0xF""017"(byte) 15

char/Character

"o"'o'

short/Short

"15""0xF""017"(short) 15

int/Integer

"15""0xF""017"15

long/Long

"15""0xF""017"15L

float/Float

"1.0"1.0f

double/Double

"1.0"1.0d

Enum 子类

"SECONDS"TimeUnit.SECONDS

java.io.File

"/path/to/file"new File("/path/to/file")

java.lang.Class

"java.lang.Integer"java.lang.Integer.class (使用 $ 表示嵌套类,例如 "java.lang.Thread$State")

java.lang.Class

"byte"byte.class (支持原始类型)

java.lang.Class

"char[]"char[].class (支持数组类型)

java.math.BigDecimal

"123.456e789"new BigDecimal("123.456e789")

java.math.BigInteger

"1234567890123456789"new BigInteger("1234567890123456789")

java.net.URI

"https://junit.java.net.cn/"URI.create("https://junit.java.net.cn/")

java.net.URL

"https://junit.java.net.cn/"URI.create("https://junit.java.net.cn/").toURL()

java.nio.charset.Charset

"UTF-8"Charset.forName("UTF-8")

java.nio.file.Path

"/path/to/file"Paths.get("/path/to/file")

java.time.Duration

"PT3S"Duration.ofSeconds(3)

java.time.Instant

"1970-01-01T00:00:00Z"Instant.ofEpochMilli(0)

java.time.LocalDateTime

"2017-03-14T12:34:56.789"LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)

java.time.LocalDate

"2017-03-14"LocalDate.of(2017, 3, 14)

java.time.LocalTime

"12:34:56.789"LocalTime.of(12, 34, 56, 789_000_000)

java.time.MonthDay

"--03-14"MonthDay.of(3, 14)

java.time.OffsetDateTime

"2017-03-14T12:34:56.789Z"OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)

java.time.OffsetTime

"12:34:56.789Z"OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)

java.time.Period

"P2M6D"Period.of(0, 2, 6)

java.time.YearMonth

"2017-03"YearMonth.of(2017, 3)

java.time.Year

"2017"Year.of(2017)

java.time.ZonedDateTime

"2017-03-14T12:34:56.789Z"ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)

java.time.ZoneId

"Europe/Berlin"ZoneId.of("Europe/Berlin")

java.time.ZoneOffset

"+02:30"ZoneOffset.ofHoursMinutes(2, 30)

java.util.Currency

"JPY"Currency.getInstance("JPY")

java.util.Locale

"en"new Locale("en")

java.util.UUID

"d043e930-7b3b-48e3-bdbe-5a3ccfb833db"UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")

回退字符串到对象转换

除了上面表格中列出的从字符串到目标类型的隐式转换之外,JUnit Jupiter 还提供了一种回退机制,用于在目标类型声明了一个适合的工厂方法工厂构造函数时,从String到给定目标类型的自动转换,如下定义。

  • 工厂方法:在目标类型中声明的非私有、static 方法,它接受一个String参数并返回目标类型的实例。方法的名称可以是任意的,不需要遵循任何特定的约定。

  • 工厂构造函数:目标类型中接受一个String参数的非私有构造函数。请注意,目标类型必须声明为顶级类或static嵌套类。

如果发现多个工厂方法,它们将被忽略。如果发现一个工厂方法和一个工厂构造函数,将使用工厂方法而不是构造函数。

例如,在下面的@ParameterizedTest方法中,Book参数将通过调用Book.fromTitle(String)工厂方法并传递"42 Cats"作为书籍的标题来创建。

@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
    assertEquals("42 Cats", book.getTitle());
}
public class Book {

    private final String title;

    private Book(String title) {
        this.title = title;
    }

    public static Book fromTitle(String title) {
        return new Book(title);
    }

    public String getTitle() {
        return this.title;
    }
}
显式转换

您可以通过使用@ConvertWith注解为特定参数显式指定要使用的ArgumentConverter,而不是依赖于隐式参数转换,如下面的示例所示。请注意,ArgumentConverter的实现必须声明为顶级类或static嵌套类。

@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(
        @ConvertWith(ToStringArgumentConverter.class) String argument) {

    assertNotNull(ChronoUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {

    @Override
    protected Object convert(Object source, Class<?> targetType) {
        assertEquals(String.class, targetType, "Can only convert to String");
        if (source instanceof Enum<?>) {
            return ((Enum<?>) source).name();
        }
        return String.valueOf(source);
    }
}

如果转换器只用于将一种类型转换为另一种类型,您可以扩展TypedArgumentConverter以避免样板类型检查。

public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {

    protected ToLengthArgumentConverter() {
        super(String.class, Integer.class);
    }

    @Override
    protected Integer convert(String source) {
        return (source != null ? source.length() : 0);
    }

}

显式参数转换器旨在由测试和扩展作者实现。因此,junit-jupiter-params只提供了一个显式参数转换器,它也可以作为参考实现:JavaTimeArgumentConverter。它通过组合注解JavaTimeConversionPattern使用。

@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(
        @JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {

    assertEquals(2017, argument.getYear());
}

如果您希望实现一个也使用注解的自定义ArgumentConverter(如JavaTimeArgumentConverter),您可以扩展AnnotationBasedArgumentConverter类。

2.16.5. 参数聚合

默认情况下,提供给@ParameterizedTest方法的每个参数对应于一个方法参数。因此,预期提供大量参数的参数源会导致方法签名过长。

在这种情况下,可以使用ArgumentsAccessor而不是多个参数。使用此 API,您可以通过传递给测试方法的单个参数访问提供的参数。此外,如隐式转换中所述,支持类型转换。

此外,您可以使用ArgumentsAccessor.getInvocationIndex()检索当前测试调用索引。

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
    Person person = new Person(arguments.getString(0),
                               arguments.getString(1),
                               arguments.get(2, Gender.class),
                               arguments.get(3, LocalDate.class));

    if (person.getFirstName().equals("Jane")) {
        assertEquals(Gender.F, person.getGender());
    }
    else {
        assertEquals(Gender.M, person.getGender());
    }
    assertEquals("Doe", person.getLastName());
    assertEquals(1990, person.getDateOfBirth().getYear());
}

ArgumentsAccessor的实例会自动注入到任何类型为ArgumentsAccessor的参数中。

自定义聚合器

除了使用ArgumentsAccessor直接访问@ParameterizedTest方法的参数之外,JUnit Jupiter 还支持使用自定义的可重用聚合器

要使用自定义聚合器,请实现ArgumentsAggregator接口,并通过@ParameterizedTest方法中兼容参数上的@AggregateWith注解注册它。然后,在调用参数化测试时,聚合的结果将作为对应参数的参数提供。请注意,ArgumentsAggregator的实现必须声明为顶级类或static嵌套类。

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
    // perform assertions against person
}
public class PersonAggregator implements ArgumentsAggregator {
    @Override
    public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
        return new Person(arguments.getString(0),
                          arguments.getString(1),
                          arguments.get(2, Gender.class),
                          arguments.get(3, LocalDate.class));
    }
}

如果您发现自己在代码库中为多个参数化测试方法重复声明@AggregateWith(MyTypeAggregator.class),您可能希望创建一个自定义的组合注解,例如@CsvToMyType,它使用@AggregateWith(MyTypeAggregator.class)进行元注解。以下示例演示了使用自定义@CsvToPerson注解的实际操作。

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
    // perform assertions against person
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}

2.16.6. 自定义显示名称

默认情况下,参数化测试调用的显示名称包含调用索引和该特定调用所有参数的String表示形式。每个参数前面都带有参数名称(除非参数仅通过ArgumentsAccessorArgumentAggregator可用),如果存在于字节码中(对于 Java,测试代码必须使用-parameters编译器标志进行编译)。

但是,您可以通过@ParameterizedTest注解的name属性自定义调用显示名称,如下面的示例所示。

@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}

当使用ConsoleLauncher执行上述方法时,您将看到类似于以下内容的输出。

Display name of container ✔
├─ 1 ==> the rank of 'apple' is 1 ✔
├─ 2 ==> the rank of 'banana' is 2 ✔
└─ 3 ==> the rank of 'lemon, lime' is 3 ✔

请注意,name是一个MessageFormat模式。因此,单引号(')需要表示为双单引号('')才能显示。

以下占位符在自定义显示名称中受支持。

占位符 描述

{displayName}

方法的显示名称

{index}

当前调用索引(从 1 开始)

{arguments}

完整的、用逗号分隔的参数列表

{argumentsWithNames}

完整的、用逗号分隔的参数列表,带有参数名称

{0}{1}、…​

单个参数

当在显示名称中包含参数时,如果它们的字符串表示形式超过配置的最大长度,则会将其截断。该限制可以通过junit.jupiter.params.displayname.argument.maxlength配置参数进行配置,默认值为 512 个字符。

当使用@MethodSource@ArgumentsSource时,您可以使用Named API 为参数提供自定义名称。如果参数包含在调用显示名称中,则将使用自定义名称,如下面的示例所示。

@DisplayName("A parameterized test with named arguments")
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("namedArguments")
void testWithNamedArguments(File file) {
}

static Stream<Arguments> namedArguments() {
    return Stream.of(
        arguments(named("An important file", new File("path1"))),
        arguments(named("Another file", new File("path2")))
    );
}
A parameterized test with named arguments ✔
├─ 1: An important file ✔
└─ 2: Another file ✔

如果您希望为项目中的所有参数化测试设置默认名称模式,您可以在junit-platform.properties文件中声明junit.jupiter.params.displayname.default配置参数,如下面的示例所示(有关其他选项,请参阅配置参数)。

junit.jupiter.params.displayname.default = {index}

参数化测试的显示名称是根据以下优先级规则确定的

  1. @ParameterizedTest中的name属性,如果存在

  2. junit.jupiter.params.displayname.default配置参数的值,如果存在

  3. @ParameterizedTest中定义的DEFAULT_DISPLAY_NAME常量

2.16.7. 生命周期和互操作性

参数化测试的每次调用都与常规的@Test方法具有相同的生命周期。例如,@BeforeEach方法将在每次调用之前执行。与动态测试类似,调用将在 IDE 的测试树中逐个出现。您可以在同一个测试类中随意混合常规的@Test方法和@ParameterizedTest方法。

您可以将ParameterResolver扩展与@ParameterizedTest方法一起使用。但是,由参数源解析的方法参数需要在参数列表中排在首位。由于测试类可能包含常规测试以及参数列表不同的参数化测试,因此不会为生命周期方法(例如@BeforeEach)和测试类构造函数解析参数源中的值。

@BeforeEach
void beforeEach(TestInfo testInfo) {
    // ...
}

@ParameterizedTest
@ValueSource(strings = "apple")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
    testReporter.publishEntry("argument", argument);
}

@AfterEach
void afterEach(TestInfo testInfo) {
    // ...
}

2.17. 测试模板

一个@TestTemplate方法不是一个普通的测试用例,而是一个测试用例的模板。因此,它被设计为根据注册的提供者返回的调用上下文数量多次调用。因此,它必须与注册的TestTemplateInvocationContextProvider扩展一起使用。每次调用测试模板方法的行为都类似于执行一个普通的@Test方法,完全支持相同的生命周期回调和扩展。有关使用示例,请参阅为测试模板提供调用上下文

重复测试参数化测试是测试模板的内置专门化。

2.18. 动态测试

JUnit Jupiter 中描述的标准@Test注解,在注解中,与 JUnit 4 中的@Test注解非常相似。两者都描述了实现测试用例的方法。这些测试用例在编译时是完全指定的,它们的运行时行为不能被任何运行时事件改变。假设提供了动态行为的基本形式,但它们的表达能力有意地相当有限。

除了这些标准测试之外,JUnit Jupiter 还引入了一种全新的测试编程模型。这种新型测试是动态测试,它是在运行时由用@TestFactory注解的工厂方法生成的。

@Test方法不同,@TestFactory方法本身不是一个测试用例,而是一个测试用例的工厂。因此,动态测试是工厂的产物。从技术上讲,@TestFactory方法必须返回单个DynamicNodeStreamCollectionIterableIteratorDynamicNode实例数组。DynamicNode的可实例化子类是DynamicContainerDynamicTestDynamicContainer实例由显示名称和动态子节点列表组成,从而能够创建任意嵌套的动态节点层次结构。DynamicTest实例将被延迟执行,从而能够动态甚至非确定性地生成测试用例。

@TestFactory返回的任何Stream都将通过调用stream.close()正确关闭,使其可以安全地使用诸如Files.lines()之类的资源。

@Test方法一样,@TestFactory方法不能是privatestatic,并且可以选择声明由ParameterResolvers解析的参数。

DynamicTest是在运行时生成的测试用例。它由显示名称Executable组成。Executable是一个@FunctionalInterface,这意味着动态测试的实现可以作为lambda 表达式方法引用提供。

动态测试生命周期
动态测试的执行生命周期与标准@Test用例的执行生命周期大不相同。具体来说,没有针对单个动态测试的生命周期回调。这意味着@BeforeEach@AfterEach方法及其对应的扩展回调将针对@TestFactory方法执行,但不会针对每个动态测试执行。换句话说,如果您在动态测试的 lambda 表达式中访问测试实例中的字段,那么这些字段不会在由同一个@TestFactory方法生成的单个动态测试执行之间被回调方法或扩展重置。

从 JUnit Jupiter 5.10.2 开始,动态测试必须始终由工厂方法创建;但是,这可能会在以后的版本中通过注册机制得到补充。

2.18.1. 动态测试示例

以下DynamicTestsDemo类演示了测试工厂和动态测试的几个示例。

第一个方法返回无效的返回类型。由于在编译时无法检测到无效的返回类型,因此在运行时检测到它时会抛出JUnitException

接下来的六个方法演示了生成CollectionIterableIterator、数组或StreamDynamicTest实例。这些示例中的大多数实际上并没有表现出动态行为,而只是原则上演示了支持的返回类型。但是,dynamicTestsFromStream()dynamicTestsFromIntStream()演示了如何为给定的一组字符串或一系列输入数字生成动态测试。

下一个方法本质上是动态的。generateRandomNumberOfTests()实现了一个Iterator,它生成随机数、显示名称生成器和测试执行器,然后将所有三个提供给DynamicTest.stream()。虽然generateRandomNumberOfTests()的非确定性行为当然与测试可重复性相冲突,因此应该谨慎使用,但它用于演示动态测试的表达能力和强大功能。

下一个方法在灵活性方面类似于generateRandomNumberOfTests();但是,dynamicTestsFromStreamFactoryMethod()通过DynamicTest.stream()工厂方法从现有的Stream生成动态测试流。

为了演示目的,dynamicNodeSingleTest()方法生成单个DynamicTest而不是流,而dynamicNodeSingleContainer()方法使用DynamicContainer生成动态测试的嵌套层次结构。

import static example.util.StringUtils.isPalindrome;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.junit.jupiter.api.Named.named;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import example.util.Calculator;

import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;

class DynamicTestsDemo {

    private final Calculator calculator = new Calculator();

    // This will result in a JUnitException!
    @TestFactory
    List<String> dynamicTestsWithInvalidReturnType() {
        return Arrays.asList("Hello");
    }

    @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
            dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        );
    }

    @TestFactory
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(
            dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
            dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        );
    }

    @TestFactory
    Iterator<DynamicTest> dynamicTestsFromIterator() {
        return Arrays.asList(
            dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
            dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        ).iterator();
    }

    @TestFactory
    DynamicTest[] dynamicTestsFromArray() {
        return new DynamicTest[] {
            dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
            dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        };
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("racecar", "radar", "mom", "dad")
            .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        // Generates tests for the first 10 even integers.
        return IntStream.iterate(0, n -> n + 2).limit(10)
            .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {

        // Generates random positive integers between 0 and 100 until
        // a number evenly divisible by 7 is encountered.
        Iterator<Integer> inputGenerator = new Iterator<Integer>() {

            Random random = new Random();
            int current;

            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }

            @Override
            public Integer next() {
                return current;
            }
        };

        // Generates display names like: input:5, input:37, input:85, etc.
        Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;

        // Executes tests based on the current input value.
        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);

        // Returns a stream of dynamic tests.
        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
        // Stream of palindromes to check
        Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");

        // Generates display names like: racecar is a palindrome
        Function<String, String> displayNameGenerator = text -> text + " is a palindrome";

        // Executes tests based on the current input value.
        ThrowingConsumer<String> testExecutor = text -> assertTrue(isPalindrome(text));

        // Returns a stream of dynamic tests.
        return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
        // Stream of palindromes to check
        Stream<Named<String>> inputStream = Stream.of(
                named("racecar is a palindrome", "racecar"),
                named("radar is also a palindrome", "radar"),
                named("mom also seems to be a palindrome", "mom"),
                named("dad is yet another palindrome", "dad")
            );

        // Returns a stream of dynamic tests.
        return DynamicTest.stream(inputStream,
            text -> assertTrue(isPalindrome(text)));
    }

    @TestFactory
    Stream<DynamicNode> dynamicTestsWithContainers() {
        return Stream.of("A", "B", "C")
            .map(input -> dynamicContainer("Container " + input, Stream.of(
                dynamicTest("not null", () -> assertNotNull(input)),
                dynamicContainer("properties", Stream.of(
                    dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                    dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
                ))
            )));
    }

    @TestFactory
    DynamicNode dynamicNodeSingleTest() {
        return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
    }

    @TestFactory
    DynamicNode dynamicNodeSingleContainer() {
        return dynamicContainer("palindromes",
            Stream.of("racecar", "radar", "mom", "dad")
                .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
        ));
    }

}

2.18.2. 动态测试的 URI 测试源

JUnit Platform 提供了TestSource,它表示测试或容器的来源,用于通过 IDE 和构建工具导航到其位置。

动态测试或动态容器的TestSource可以从java.net.URI构建,该URI可以通过DynamicTest.dynamicTest(String, URI, Executable)DynamicContainer.dynamicContainer(String, URI, Stream)工厂方法分别提供。URI将被转换为以下TestSource实现之一。

ClasspathResourceSource

如果URI包含classpath方案,例如classpath:/test/foo.xml?line=20,column=2

DirectorySource

如果URI表示文件系统中存在的目录。

FileSource

如果URI表示文件系统中存在的文件。

MethodSource

如果URI包含method方案和完全限定的方法名称 (FQMN),例如method:org.junit.Foo#bar(java.lang.String, java.lang.String[])。有关 FQMN 支持的格式,请参阅DiscoverySelectors.selectMethod(String)的 Javadoc。

ClassSource

如果URI包含class方案和完全限定的类名,例如class:org.junit.Foo?line=42

UriSource

如果上述TestSource实现都不适用。

2.19. 超时

@Timeout注解允许声明测试、测试工厂、测试模板或生命周期方法在执行时间超过给定持续时间时应该失败。持续时间的单位默认为秒,但可以配置。

以下示例展示了如何将@Timeout应用于生命周期和测试方法。

class TimeoutDemo {

    @BeforeEach
    @Timeout(5)
    void setUp() {
        // fails if execution time exceeds 5 seconds
    }

    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    void failsIfExecutionTimeExceeds500Milliseconds() {
        // fails if execution time exceeds 500 milliseconds
    }

    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS, threadMode = ThreadMode.SEPARATE_THREAD)
    void failsIfExecutionTimeExceeds500MillisecondsInSeparateThread() {
        // fails if execution time exceeds 500 milliseconds, the test code is executed in a separate thread
    }

}

要将相同的超时应用于测试类及其所有@Nested类中的所有测试方法,可以在类级别声明@Timeout注解。然后,它将应用于该类及其@Nested类中的所有测试、测试工厂和测试模板方法,除非被特定方法或@Nested类上的@Timeout注解覆盖。请注意,在类级别声明的@Timeout注解不应用于生命周期方法。

@TestFactory方法上声明@Timeout会检查工厂方法是否在指定持续时间内返回,但不会验证工厂生成的每个单独的DynamicTest的执行时间。为此,请使用assertTimeout()assertTimeoutPreemptively()

如果@Timeout存在于@TestTemplate方法上,例如@RepeatedTest@ParameterizedTest,则每次调用都会应用给定的超时。

2.19.1. 线程模式

超时可以使用以下三种线程模式之一应用:SAME_THREADSEPARATE_THREADINFERRED

当使用SAME_THREAD时,注解方法的执行在测试的主线程中进行。如果超时,主线程将从另一个线程中断。这样做是为了确保与 Spring 等框架的互操作性,这些框架使用对当前运行线程敏感的机制,例如ThreadLocal事务管理。

相反,当使用SEPARATE_THREAD时,就像assertTimeoutPreemptively()断言一样,注解方法的执行在单独的线程中进行,这会导致不良副作用,请参阅使用assertTimeoutPreemptively()的抢占式超时

当使用INFERRED(默认)线程模式时,线程模式通过junit.jupiter.execution.timeout.thread.mode.default配置参数解析。如果提供的配置参数无效或不存在,则使用SAME_THREAD作为回退。

2.19.2. 默认超时

以下配置参数可用于指定所有类别方法的默认超时,除非它们或封闭的测试类用@Timeout注解。

junit.jupiter.execution.timeout.default

所有可测试和生命周期方法的默认超时

junit.jupiter.execution.timeout.testable.method.default

所有可测试方法的默认超时

junit.jupiter.execution.timeout.test.method.default

@Test方法的默认超时

junit.jupiter.execution.timeout.testtemplate.method.default

@TestTemplate方法的默认超时

junit.jupiter.execution.timeout.testfactory.method.default

@TestFactory方法的默认超时

junit.jupiter.execution.timeout.lifecycle.method.default

所有生命周期方法的默认超时

junit.jupiter.execution.timeout.beforeall.method.default

@BeforeAll方法的默认超时

junit.jupiter.execution.timeout.beforeeach.method.default

@BeforeEach方法的默认超时

junit.jupiter.execution.timeout.aftereach.method.default

@AfterEach方法的默认超时

junit.jupiter.execution.timeout.afterall.method.default

@AfterAll方法的默认超时

更具体的配置参数会覆盖不太具体的配置参数。例如,junit.jupiter.execution.timeout.test.method.default会覆盖junit.jupiter.execution.timeout.testable.method.default,而junit.jupiter.execution.timeout.testable.method.default会覆盖junit.jupiter.execution.timeout.default

此类配置参数的值必须采用以下不区分大小写的格式:<number> [ns|μs|ms|s|m|h|d]。数字和单位之间的空格可以省略。不指定单位等效于使用秒。

表 1. 超时配置参数值的示例
参数值 等效注解

42

@Timeout(42)

42 ns

@Timeout(value = 42, unit = NANOSECONDS)

42 μs

@Timeout(value = 42, unit = MICROSECONDS)

42 ms

@Timeout(value = 42, unit = MILLISECONDS)

42 s

@Timeout(value = 42, unit = SECONDS)

42 分钟

@Timeout(value = 42, unit = MINUTES)

42 小时

@Timeout(value = 42, unit = HOURS)

42 天

@Timeout(value = 42, unit = DAYS)

2.19.3. 使用 @Timeout 进行轮询测试

在处理异步代码时,通常会编写在等待某些事件发生之前进行轮询的测试,然后执行任何断言。在某些情况下,您可以重写逻辑以使用 CountDownLatch 或其他同步机制,但有时这不可行 - 例如,如果被测对象向外部消息代理中的通道发送消息,并且只有在消息成功通过通道发送后才能执行断言。像这样的异步测试需要某种形式的超时,以确保它们不会通过无限执行来挂起测试套件,就像异步消息永远无法成功传递一样。

通过为轮询的异步测试配置超时,您可以确保测试不会无限执行。以下示例演示了如何使用 JUnit Jupiter 的 @Timeout 注解来实现这一点。此技术可以非常轻松地用于实现“轮询直到”逻辑。

@Test
@Timeout(5) // Poll at most 5 seconds
void pollUntil() throws InterruptedException {
    while (asynchronousResultNotAvailable()) {
        Thread.sleep(250); // custom poll interval
    }
    // Obtain the asynchronous result and perform assertions
}
如果您需要对轮询间隔进行更多控制,并对异步测试有更大的灵活性,请考虑使用专门的库,例如 Awaitility

2.19.4. 全局禁用 @Timeout

在调试会话中逐步执行代码时,固定的超时限制可能会影响测试结果,例如,即使所有断言都满足,也会将测试标记为失败。

JUnit Jupiter 支持 junit.jupiter.execution.timeout.mode 配置参数来配置何时应用超时。有三种模式:enableddisableddisabled_on_debug。默认模式为 enabled。当 VM 运行时的某个输入参数以 -agentlib:jdwp-Xrunjdwp 开头时,它被认为是在调试模式下运行。此启发式方法由 disabled_on_debug 模式查询。

2.20. 并行执行

默认情况下,JUnit Jupiter 测试在单个线程中按顺序运行。从 5.3 版本开始,并行运行测试(例如,为了加快执行速度)作为一种可选功能提供。要启用并行执行,请将 junit.jupiter.execution.parallel.enabled 配置参数设置为 true - 例如,在 junit-platform.properties 中(有关其他选项,请参阅 配置参数)。

请注意,启用此属性只是并行执行测试所需的第一个步骤。如果启用,测试类和方法默认情况下仍将按顺序执行。测试树中的节点是否并发执行由其执行模式控制。以下两种模式可用。

SAME_THREAD

强制在与父节点相同的线程中执行。例如,当在测试方法上使用时,测试方法将在与包含测试类的任何 @BeforeAll@AfterAll 方法相同的线程中执行。

CONCURRENT

并发执行,除非资源锁强制在同一线程中执行。

默认情况下,测试树中的节点使用 SAME_THREAD 执行模式。您可以通过设置 junit.jupiter.execution.parallel.mode.default 配置参数来更改默认值。或者,您可以使用 @Execution 注解来更改注释元素及其子元素(如果有)的执行模式,这使您可以为单个测试类逐个激活并行执行。

配置参数以并行执行所有测试
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

默认执行模式应用于测试树的所有节点,但有一些值得注意的例外,即使用 Lifecycle.PER_CLASS 模式或 MethodOrderer(除了 MethodOrderer.Random)的测试类。在前一种情况下,测试作者必须确保测试类是线程安全的;在后一种情况下,并发执行可能会与配置的执行顺序冲突。因此,在这两种情况下,只有在测试类或方法上存在 @Execution(CONCURRENT) 注解时,此类测试类中的测试方法才会并发执行。

当启用并行执行并注册默认 ClassOrderer 时(有关详细信息,请参阅 类顺序),顶级测试类将首先按顺序排序并按该顺序调度。但是,不能保证它们会完全按该顺序启动,因为执行它们的线程不受 JUnit 的直接控制。

配置为 CONCURRENT 执行模式的测试树的所有节点将根据提供的 配置 并行完全执行,同时观察声明式 同步 机制。请注意,需要单独启用 捕获标准输出/错误

此外,您可以通过设置 junit.jupiter.execution.parallel.mode.classes.default 配置参数来配置顶级类的默认执行模式。通过组合这两个配置参数,您可以配置类以并行运行,但它们的方法在同一线程中运行

配置参数以并行执行顶级类,但方法在同一线程中
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent

相反的组合将并行运行一个类中的所有方法,但顶级类将按顺序运行

配置参数以按顺序执行顶级类,但并行执行其方法
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread

下图说明了两个顶级测试类 AB(每个类有两个测试方法)的执行方式,针对 junit.jupiter.execution.parallel.mode.defaultjunit.jupiter.execution.parallel.mode.classes.default 的所有四种组合(请参阅第一列中的标签)。

writing tests execution mode
默认执行模式配置组合

如果未显式设置 junit.jupiter.execution.parallel.mode.classes.default 配置参数,则将使用 junit.jupiter.execution.parallel.mode.default 的值。

2.20.1. 配置

可以使用 ParallelExecutionConfigurationStrategy 配置属性,例如所需的并行度和最大池大小。JUnit Platform 提供了两种开箱即用的实现:dynamicfixed。或者,您可以实现一个 custom 策略。

要选择策略,请将 junit.jupiter.execution.parallel.config.strategy 配置参数设置为以下选项之一。

dynamic

根据可用处理器/内核数量乘以 junit.jupiter.execution.parallel.config.dynamic.factor 配置参数(默认为 1)来计算所需的并行度。可选的 junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor 配置参数可用于限制最大线程数。

fixed

使用强制的 junit.jupiter.execution.parallel.config.fixed.parallelism 配置参数作为所需的并行度。可选的 junit.jupiter.execution.parallel.config.fixed.max-pool-size 配置参数可用于限制最大线程数。

custom

允许您通过强制的 junit.jupiter.execution.parallel.config.custom.class 配置参数指定自定义 ParallelExecutionConfigurationStrategy 实现,以确定所需的配置。

如果未设置任何配置策略,JUnit Jupiter 将使用 dynamic 配置策略,其因子为 1。因此,所需的并行度将等于可用处理器/内核的数量。

并行度本身并不意味着最大并发线程数
默认情况下,JUnit Jupiter 不保证并发执行的测试数量不会超过配置的并行度。例如,当使用下一节中描述的同步机制之一时,幕后使用的 ForkJoinPool 可能会生成额外的线程以确保执行以足够的并行度继续。如果您需要此类保证,使用 Java 9+,可以通过控制 dynamicfixedcustom 策略的最大池大小来限制最大并发线程数。
相关属性

下表列出了用于配置并行执行的相关属性。有关如何设置此类属性的详细信息,请参阅 配置参数

属性 描述 支持的值 默认值

junit.jupiter.execution.parallel.enabled

启用并行测试执行

  • true

  • false

false

junit.jupiter.execution.parallel.mode.default

测试树中节点的默认执行模式

  • concurrent

  • same_thread

same_thread

junit.jupiter.execution.parallel.mode.classes.default

顶级类的默认执行模式

  • concurrent

  • same_thread

same_thread

junit.jupiter.execution.parallel.config.strategy

用于所需并行度和最大池大小的执行策略

  • dynamic

  • fixed

  • custom

dynamic

junit.jupiter.execution.parallel.config.dynamic.factor

用于确定 dynamic 配置策略的所需并行度的因子,该因子乘以可用处理器/内核的数量

正小数

1.0

junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor

用于确定 dynamic 配置策略的所需并行度的因子,该因子乘以可用处理器/内核的数量和 junit.jupiter.execution.parallel.config.dynamic.factor 的值

正小数,必须大于或等于 1.0

256 + junit.jupiter.execution.parallel.config.dynamic.factor 的值乘以可用处理器/内核的数量

junit.jupiter.execution.parallel.config.dynamic.saturate

禁用 dynamic 配置策略的基础 fork-join 池的饱和

  • true

  • false

true

junit.jupiter.execution.parallel.config.fixed.parallelism

fixed 配置策略的所需并行度

正整数

无默认值

junit.jupiter.execution.parallel.config.fixed.max-pool-size

fixed 配置策略的基础 fork-join 池的所需最大池大小

正整数,必须大于或等于 junit.jupiter.execution.parallel.config.fixed.parallelism

256 + junit.jupiter.execution.parallel.config.fixed.parallelism 的值

junit.jupiter.execution.parallel.config.fixed.saturate

禁用 fixed 配置策略的基础 fork-join 池的饱和

  • true

  • false

true

junit.jupiter.execution.parallel.config.custom.class

要用于 custom 配置策略的 ParallelExecutionConfigurationStrategy 的完全限定类名

例如,org.example.CustomStrategy

无默认值

2.20.2. 同步

除了使用@Execution注解控制执行模式之外,JUnit Jupiter还提供另一种基于注解的声明式同步机制。@ResourceLock注解允许您声明测试类或方法使用特定共享资源,该资源需要同步访问以确保可靠的测试执行。共享资源由一个唯一的名称标识,该名称是一个String。该名称可以是用户定义的,也可以是Resources中的预定义常量之一:SYSTEM_PROPERTIESSYSTEM_OUTSYSTEM_ERRLOCALETIME_ZONE

如果以下示例中的测试在没有使用@ResourceLock的情况下并行运行,它们将是不稳定的。有时它们会通过,有时它们会由于写入和读取同一个 JVM 系统属性的固有竞争条件而失败。

当使用@ResourceLock注解声明对共享资源的访问时,JUnit Jupiter 引擎将使用此信息来确保没有冲突的测试并行运行。

隔离运行测试

如果您的大多数测试类可以在没有任何同步的情况下并行运行,但您有一些测试类需要隔离运行,您可以使用@Isolated注解标记后者。这些类中的测试将按顺序执行,没有任何其他测试同时运行。

除了唯一标识共享资源的String之外,您还可以指定访问模式。两个需要对共享资源进行READ访问的测试可以彼此并行运行,但不能在任何其他需要对同一个共享资源进行READ_WRITE访问的测试运行时并行运行。

@Execution(CONCURRENT)
class SharedResourcesDemo {

    private Properties backup;

    @BeforeEach
    void backup() {
        backup = new Properties();
        backup.putAll(System.getProperties());
    }

    @AfterEach
    void restore() {
        System.setProperties(backup);
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
    void customPropertyIsNotSetByDefault() {
        assertNull(System.getProperty("my.prop"));
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
    void canSetCustomPropertyToApple() {
        System.setProperty("my.prop", "apple");
        assertEquals("apple", System.getProperty("my.prop"));
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
    void canSetCustomPropertyToBanana() {
        System.setProperty("my.prop", "banana");
        assertEquals("banana", System.getProperty("my.prop"));
    }

}

2.21. 内置扩展

虽然 JUnit 团队鼓励将可重用扩展打包并在单独的库中维护,但 JUnit Jupiter API 工件包含一些面向用户的扩展实现,这些实现被认为是如此普遍有用,以至于用户不必添加另一个依赖项。

2.21.1. TempDirectory 扩展

内置的TempDirectory扩展用于为单个测试或测试类中的所有测试创建和清理临时目录。它默认注册。要使用它,请使用@TempDir注解类型为java.nio.file.Pathjava.io.File的非 final、未分配字段,或向生命周期方法或测试方法添加类型为java.nio.file.Pathjava.io.File的参数,并使用@TempDir注解。

例如,以下测试声明了一个使用@TempDir注解的参数,用于单个测试方法,在临时目录中创建并写入文件,并检查其内容。

需要临时目录的测试方法
@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
    Path file = tempDir.resolve("test.txt");

    new ListWriter(file).write("a", "b", "c");

    assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}

您可以通过指定多个带注解的参数来注入多个临时目录。

需要多个临时目录的测试方法
@Test
void copyFileFromSourceToTarget(@TempDir Path source, @TempDir Path target) throws IOException {
    Path sourceFile = source.resolve("test.txt");
    new ListWriter(sourceFile).write("a", "b", "c");

    Path targetFile = Files.copy(sourceFile, target.resolve("test.txt"));

    assertNotEquals(sourceFile, targetFile);
    assertEquals(singletonList("a,b,c"), Files.readAllLines(targetFile));
}
要恢复使用单个临时目录(根据注解使用级别,用于整个测试类或方法)的旧行为,您可以将junit.jupiter.tempdir.scope配置参数设置为per_context。但是,请注意,此选项已弃用,将在将来的版本中删除。

@TempDir不支持构造函数参数。如果您希望在生命周期方法和当前测试方法中保留对临时目录的单个引用,请通过使用@TempDir注解实例字段来使用字段注入。

以下示例将共享临时目录存储在static字段中。这允许在测试类的所有生命周期方法和测试方法中使用相同的sharedTempDir。为了更好地隔离,您应该使用实例字段,以便每个测试方法使用单独的目录。

在测试方法之间共享临时目录的测试类
class SharedTempDirectoryDemo {

    @TempDir
    static Path sharedTempDir;

    @Test
    void writeItemsToFile() throws IOException {
        Path file = sharedTempDir.resolve("test.txt");

        new ListWriter(file).write("a", "b", "c");

        assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
    }

    @Test
    void anotherTestThatUsesTheSameTempDir() {
        // use sharedTempDir
    }

}

@TempDir注解有一个可选的cleanup属性,可以设置为NEVERON_SUCCESSALWAYS。如果清理模式设置为NEVER,则在测试完成后不会删除临时目录。如果设置为ON_SUCCESS,则仅在测试成功完成后才会删除临时目录。

默认清理模式为ALWAYS。您可以使用junit.jupiter.tempdir.cleanup.mode.default配置参数来覆盖此默认值。

具有不会被清理的临时目录的测试类
class CleanupModeDemo {

    @Test
    void fileTest(@TempDir(cleanup = ON_SUCCESS) Path tempDir) {
        // perform test
    }

}

@TempDir支持通过可选的factory属性以编程方式创建临时目录。这通常用于控制临时目录的创建,例如定义父目录或应使用的文件系统。

工厂可以通过实现TempDirFactory来创建。实现必须提供一个无参数构造函数,并且不应对何时以及如何多次实例化它们做出任何假设,但它们可以假设它们的createTempDirectory(…​)close()方法都将被调用一次,按此顺序,并且来自同一个线程。

Jupiter 中可用的默认实现将目录创建委托给java.nio.file.Files::createTempDirectory,并将junit作为前缀字符串传递,用于生成目录的名称。

以下示例定义了一个工厂,该工厂使用测试名称作为目录名称前缀,而不是junit常量值。

具有使用测试名称作为目录名称前缀的临时目录的测试类
class TempDirFactoryDemo {

    @Test
    void factoryTest(@TempDir(factory = Factory.class) Path tempDir) {
        assertTrue(tempDir.getFileName().toString().startsWith("factoryTest"));
    }

    static class Factory implements TempDirFactory {

        @Override
        public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
                throws IOException {
            return Files.createTempDirectory(extensionContext.getRequiredTestMethod().getName());
        }

    }

}

也可以使用内存文件系统(如Jimfs)来创建临时目录。以下示例演示了如何实现这一点。

使用 Jimfs 内存文件系统创建的临时目录的测试类
class InMemoryTempDirDemo {

    @Test
    void test(@TempDir(factory = JimfsTempDirFactory.class) Path tempDir) {
        // perform test
    }

    static class JimfsTempDirFactory implements TempDirFactory {

        private final FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());

        @Override
        public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
                throws IOException {
            return Files.createTempDirectory(fileSystem.getPath("/"), "junit");
        }

        @Override
        public void close() throws IOException {
            fileSystem.close();
        }

    }

}

@TempDir也可以用作元注解以减少重复。以下代码清单显示了如何创建自定义的@JimfsTempDir注解,该注解可以用作@TempDir(factory = JimfsTempDirFactory.class)的直接替换。

使用@TempDir元注解的自定义注解
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@TempDir(factory = JimfsTempDirFactory.class)
@interface JimfsTempDir {
}

以下示例演示了如何使用自定义的@JimfsTempDir注解。

使用自定义注解的测试类
class JimfsTempDirAnnotationDemo {

    @Test
    void test(@JimfsTempDir Path tempDir) {
        // perform test
    }

}

TempDir注解声明的字段或参数上的元注解或附加注解可能会公开其他属性来配置工厂。这些注解和相关属性可以通过createTempDirectoryAnnotatedElementContext参数访问。

您可以使用junit.jupiter.tempdir.factory.default配置参数来指定您希望默认使用的TempDirFactory的完全限定类名。与通过@TempDir注解的factory属性配置的工厂一样,提供的类必须实现TempDirFactory接口。默认工厂将用于所有@TempDir注解,除非注解的factory属性指定了不同的工厂。

总之,临时目录的工厂是根据以下优先级规则确定的

  1. 如果存在,则为@TempDir注解的factory属性

  2. 如果存在,则为通过配置参数配置的默认TempDirFactory

  3. 否则,将使用org.junit.jupiter.api.io.TempDirFactory$Standard

3. 从 JUnit 4 迁移

虽然 JUnit Jupiter 编程模型和扩展模型不支持 JUnit 4 功能(如RulesRunners),但预计源代码维护人员无需更新其所有现有测试、测试扩展和自定义构建测试基础设施来迁移到 JUnit Jupiter。

相反,JUnit 通过JUnit Vintage 测试引擎提供了一个平滑的迁移路径,该引擎允许基于 JUnit 3 和 JUnit 4 的现有测试使用 JUnit Platform 基础设施执行。由于所有特定于 JUnit Jupiter 的类和注解都位于org.junit.jupiter基本包下,因此在类路径中同时存在 JUnit 4 和 JUnit Jupiter 不会导致任何冲突。因此,可以安全地将现有的 JUnit 4 测试与 JUnit Jupiter 测试一起维护。此外,由于 JUnit 团队将继续为 JUnit 4.x 基线提供维护和错误修复版本,因此开发人员有充足的时间根据自己的时间表迁移到 JUnit Jupiter。

3.1. 在 JUnit Platform 上运行 JUnit 4 测试

确保junit-vintage-engine工件位于您的测试运行时路径中。在这种情况下,JUnit 3 和 JUnit 4 测试将自动被 JUnit Platform 启动器拾取。

请参阅junit5-samples存储库中的示例项目,了解如何使用 Gradle 和 Maven 完成此操作。

3.1.1. 类别支持

对于使用@Category注解的测试类或方法,JUnit Vintage 测试引擎将类别的完全限定类名公开为对应测试类或测试方法的标签。例如,如果一个测试方法使用@Category(Example.class)注解,它将被标记为"com.acme.Example"。与 JUnit 4 中的Categories运行器类似,此信息可用于在执行测试之前过滤发现的测试(有关详细信息,请参阅运行测试)。

3.2. 迁移提示

以下是在将现有的 JUnit 4 测试迁移到 JUnit Jupiter 时应注意的主题。

  • 注解位于org.junit.jupiter.api包中。

  • 断言位于org.junit.jupiter.api.Assertions中。

    • 请注意,您可以继续使用来自org.junit.Assert的断言方法,或使用任何其他断言库,例如AssertJHamcrestTruth等。

  • 假设位于org.junit.jupiter.api.Assumptions中。

    • 请注意,JUnit Jupiter 5.4 及更高版本支持来自 JUnit 4 的org.junit.Assume类的假设方法。具体来说,JUnit Jupiter 支持 JUnit 4 的AssumptionViolatedException来表示应中止测试,而不是将其标记为失败。

  • @Before@After不再存在;请改用@BeforeEach@AfterEach

  • @BeforeClass@AfterClass不再存在;请改用@BeforeAll@AfterAll

  • @Ignore不再存在:请改用@Disabled或其他内置的执行条件

  • @Category不再存在;请改用@Tag

  • @RunWith不再存在;被@ExtendWith取代。

  • @Rule@ClassRule不再存在;被@ExtendWith@RegisterExtension取代。

  • @Test(expected = …​)ExpectedException规则不再存在;请改用Assertions.assertThrows(…​)

  • JUnit Jupiter 中的断言和假设接受失败消息作为其最后一个参数,而不是第一个参数。

3.3. 有限的 JUnit 4 规则支持

如上所述,JUnit Jupiter 本身不支持也不打算支持 JUnit 4 的规则。然而,JUnit 团队意识到,许多组织,尤其是大型组织,可能拥有大量使用自定义规则的 JUnit 4 代码库。为了服务这些组织并提供逐步迁移路径,JUnit 团队决定在 JUnit Jupiter 中直接支持部分 JUnit 4 规则。这种支持基于适配器,仅限于语义上与 JUnit Jupiter 扩展模型兼容的规则,即那些不会完全改变测试整体执行流程的规则。

JUnit Jupiter 中的 junit-jupiter-migrationsupport 模块目前支持以下三种 Rule 类型,包括这些类型的子类

  • org.junit.rules.ExternalResource(包括 org.junit.rules.TemporaryFolder

  • org.junit.rules.Verifier(包括 org.junit.rules.ErrorCollector

  • org.junit.rules.ExpectedException

与 JUnit 4 中一样,支持使用 Rule 注解的字段和方法。通过在测试类上使用这些类级别扩展,可以保持遗留代码库中的 Rule 实现不变,包括 JUnit 4 规则导入语句。

这种有限的 Rule 支持可以通过类级别注解 @EnableRuleMigrationSupport 开启。此注解是一个组合注解,它启用所有规则迁移支持扩展:VerifierSupportExternalResourceSupportExpectedExceptionSupport。您也可以选择使用 @EnableJUnit4MigrationSupport 注解您的测试类,它会注册规则的迁移支持以及 JUnit 4 的 @Ignore 注解(参见 JUnit 4 @Ignore 支持)。

但是,如果您打算为 JUnit Jupiter 开发新的扩展,请使用 JUnit Jupiter 的新扩展模型,而不是 JUnit 4 的基于规则的模型。

3.4. JUnit 4 @Ignore 支持

为了提供从 JUnit 4 到 JUnit Jupiter 的平滑迁移路径,junit-jupiter-migrationsupport 模块提供了对 JUnit 4 的 @Ignore 注解的支持,类似于 Jupiter 的 @Disabled 注解。

要在基于 JUnit Jupiter 的测试中使用 @Ignore,请在您的构建中配置对 junit-jupiter-migrationsupport 模块的测试依赖,然后使用 @ExtendWith(IgnoreCondition.class)@EnableJUnit4MigrationSupport(它会自动注册 IgnoreCondition 以及 有限的 JUnit 4 规则支持)注解您的测试类。IgnoreCondition 是一个 ExecutionCondition,它会禁用使用 @Ignore 注解的测试类或测试方法。

import org.junit.Ignore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.migrationsupport.EnableJUnit4MigrationSupport;

// @ExtendWith(IgnoreCondition.class)
@EnableJUnit4MigrationSupport
class IgnoredTestsDemo {

    @Ignore
    @Test
    void testWillBeIgnored() {
    }

    @Test
    void testWillBeExecuted() {
    }
}

3.5. 错误消息参数

JUnit Jupiter 中的 AssumptionsAssertions 类声明参数的顺序与 JUnit 4 中不同。在 JUnit 4 中,断言和假设方法接受错误消息作为第一个参数;而在 JUnit Jupiter 中,断言和假设方法接受错误消息作为最后一个参数。

例如,JUnit 4 中的 assertEquals 方法声明为 assertEquals(String message, Object expected, Object actual),但在 JUnit Jupiter 中,它声明为 assertEquals(Object expected, Object actual, String message)。这样做的原因是错误消息是可选的,可选参数应该在方法签名中声明在必需参数之后。

受此更改影响的方法如下

  • 断言

    • assertTrue

    • assertFalse

    • assertNull

    • assertNotNull

    • assertEquals

    • assertNotEquals

    • assertArrayEquals

    • assertSame

    • assertNotSame

    • assertThrows

  • 假设

    • assumeTrue

    • assumeFalse

4. 运行测试

4.1. IDE 支持

4.1.1. IntelliJ IDEA

IntelliJ IDEA 从 2016.2 版本开始支持在 JUnit Platform 上运行测试。有关更多信息,请参阅此 IntelliJ IDEA 资源。但是,建议使用 IDEA 2017.3 或更高版本,因为 IDEA 的较新版本会根据项目中使用的 API 版本自动下载以下 JAR 文件:junit-platform-launcherjunit-jupiter-enginejunit-vintage-engine

IntelliJ IDEA 2017.3 之前的版本捆绑了特定版本的 JUnit 5。因此,如果您想使用更新版本的 JUnit Jupiter,IDE 中的测试执行可能会因版本冲突而失败。在这种情况下,请按照以下说明使用比 IntelliJ IDEA 捆绑的版本更新的 JUnit 5 版本。

为了使用不同的 JUnit 5 版本(例如,5.10.2),您可能需要将相应的 junit-platform-launcherjunit-jupiter-enginejunit-vintage-engine JAR 文件包含在类路径中。

其他 Gradle 依赖项
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher") {
  because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions")
}
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
其他 Maven 依赖项
<!-- ... -->
<dependencies>
    <!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-launcher</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.10.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

4.1.2. Eclipse

Eclipse IDE 从 Eclipse Oxygen.1a (4.7.1a) 版本开始支持 JUnit Platform。

有关在 Eclipse 中使用 JUnit 5 的更多信息,请参阅 Eclipse Project Oxygen.1a (4.7.1a) - 新功能和值得注意的更改 文档中的官方Eclipse 对 JUnit 5 的支持部分。

4.1.3. NetBeans

NetBeans 从 Apache NetBeans 10.0 版本 开始支持 JUnit Jupiter 和 JUnit Platform。

有关更多信息,请参阅 Apache NetBeans 10.0 发行说明 中的 JUnit 5 部分。

4.1.4. Visual Studio Code

Visual Studio Code 通过 Java Test Runner 扩展支持 JUnit Jupiter 和 JUnit Platform,该扩展默认情况下作为 Java Extension Pack 的一部分安装。

有关更多信息,请参阅 Java in Visual Studio Code 文档中的测试部分。

4.1.5. 其他 IDE

如果您使用的是除上述部分列出的 IDE 之外的编辑器或 IDE,JUnit 团队提供了两种替代解决方案来帮助您使用 JUnit 5。您可以手动使用 控制台启动器(例如,从命令行)或使用 基于 JUnit 4 的运行器 执行测试,如果您的 IDE 内置支持 JUnit 4。

4.2. 构建支持

4.2.1. Gradle

4.6 版本 开始,Gradle 提供了 对在 JUnit Platform 上执行测试的原生支持。要启用它,您需要在 build.gradle 中的 test 任务声明中指定 useJUnitPlatform()

test {
    useJUnitPlatform()
}

还支持按 标签标签表达式 或引擎进行过滤

test {
    useJUnitPlatform {
        includeTags("fast", "smoke & feature-a")
        // excludeTags("slow", "ci")
        includeEngines("junit-jupiter")
        // excludeEngines("junit-vintage")
    }
}

有关选项的完整列表,请参阅 官方 Gradle 文档

对齐依赖项版本

除非您使用 Spring Boot,它定义了自己的依赖项管理方式,否则建议使用 JUnit Platform BOM 来对齐所有 JUnit 5 工件的版本。

dependencies {
    testImplementation(platform("org.junit:junit-bom:5.10.2"))
}

使用 BOM 允许您在声明对所有具有 org.junit.platformorg.junit.jupiterorg.junit.vintage 组 ID 的工件的依赖项时省略版本。

有关如何在 Spring Boot 应用程序中覆盖 JUnit 版本的详细信息,请参见 Spring Boot
配置参数

标准 Gradle test 任务目前没有提供专门的 DSL 来设置 JUnit Platform 配置参数 以影响测试发现和执行。但是,您可以通过系统属性(如下所示)或通过 junit-platform.properties 文件在构建脚本中提供配置参数。

test {
    // ...
    systemProperty("junit.jupiter.conditions.deactivate", "*")
    systemProperty("junit.jupiter.extensions.autodetection.enabled", true)
    systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class")
    // ...
}
配置测试引擎

为了运行任何测试,必须在类路径上存在 TestEngine 实现。

要配置对基于 JUnit Jupiter 的测试的支持,请配置对类似于以下内容的依赖项聚合 JUnit Jupiter 工件的 testImplementation 依赖项。

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") // version can be omitted when using the BOM
}

只要您配置了对 JUnit 4 的 testImplementation 依赖项和对 JUnit Vintage TestEngine 实现的 testRuntimeOnly 依赖项,JUnit Platform 就可以运行基于 JUnit 4 的测试,类似于以下内容。

dependencies {
    testImplementation("junit:junit:4.13.2")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2") // version can be omitted when using the BOM
}
配置日志记录(可选)

JUnit 使用 java.util.logging 包(也称为JUL)中的 Java 日志记录 API 来发出警告和调试信息。有关配置选项,请参阅 LogManager 的官方文档。

或者,可以将日志消息重定向到其他日志记录框架,例如 Log4jLogback。要使用提供 LogManager 自定义实现的日志记录框架,请将 java.util.logging.manager 系统属性设置为要使用的 LogManager 实现的完全限定类名。以下示例演示了如何配置 Log4j 2.x(有关详细信息,请参见 Log4j JDK 日志记录适配器)。

test {
    systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
}

其他日志记录框架提供了不同的方法来重定向使用 java.util.logging 记录的消息。例如,对于 Logback,您可以使用 JUL 到 SLF4J 桥接,方法是将额外的依赖项添加到运行时类路径。

4.2.2. Maven

2.22.0 版本 开始,Maven Surefire 和 Maven Failsafe 提供了 对在 JUnit Platform 上执行测试的原生支持junit5-jupiter-starter-maven 项目中的 pom.xml 文件演示了如何使用 Maven Surefire 插件,可以作为配置 Maven 构建的起点。

使用 Maven Surefire/Failsafe 3.0.0-M4 或更高版本以避免互操作性问题

Maven Surefire/Failsafe 3.0.0-M4 引入了对将它使用的 JUnit Platform Launcher 的版本与在测试运行时类路径中找到的 JUnit Platform 版本对齐的支持。因此,建议使用 3.0.0-M4 或更高版本以避免互操作性问题。

或者,您可以将匹配版本的 JUnit Platform Launcher 的测试依赖项添加到您的 Maven 构建中,如下所示。

<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.10.2</version>
    <scope>test</scope>
</dependency>
对齐依赖项版本

除非您使用 Spring Boot,它定义了自己的依赖项管理方式,否则建议使用 JUnit Platform BOM 来对齐所有 JUnit 5 工件的版本。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.10.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

使用 BOM 允许您在声明对所有具有 org.junit.platformorg.junit.jupiterorg.junit.vintage 组 ID 的工件的依赖项时省略版本。

有关如何在 Spring Boot 应用程序中覆盖 JUnit 版本的详细信息,请参见 Spring Boot
配置测试引擎

为了让 Maven Surefire 或 Maven Failsafe 运行任何测试,必须将至少一个 TestEngine 实现添加到测试类路径中。

要配置对基于 JUnit Jupiter 的测试的支持,请在 JUnit Jupiter API 和 JUnit Jupiter TestEngine 实现上配置 test 范围的依赖项,类似于以下示例。

<!-- ... -->
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.2</version> <!-- can be omitted when using the BOM -->
        <scope>test</scope>
    </dependency>
    <!-- ... -->
</dependencies>
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>3.1.2</version>
        </plugin>
    </plugins>
</build>
<!-- ... -->

只要您在 JUnit 4 和 JUnit Vintage TestEngine 实现上配置 test 范围的依赖项,Maven Surefire 和 Maven Failsafe 就可以与 Jupiter 测试一起运行基于 JUnit 4 的测试,类似于以下示例。

<!-- ... -->
<dependencies>
    <!-- ... -->
    <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>5.10.2</version> <!-- can be omitted when using the BOM -->
        <scope>test</scope>
    </dependency>
    <!-- ... -->
</dependencies>
<!-- ... -->
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>3.1.2</version>
        </plugin>
    </plugins>
</build>
<!-- ... -->
按测试类名称筛选

Maven Surefire 插件将扫描其完全限定名称与以下模式匹配的测试类。

  • **/Test*.java

  • **/*Test.java

  • **/*Tests.java

  • **/*TestCase.java

此外,默认情况下,它将排除所有嵌套类(包括静态成员类)。

但是,请注意,您可以通过在 pom.xml 文件中配置显式的 includeexclude 规则来覆盖此默认行为。例如,要阻止 Maven Surefire 排除静态成员类,您可以覆盖其排除规则,如下所示。

覆盖 Maven Surefire 的排除规则
<!-- ... -->
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version>
            <configuration>
                <excludes>
                    <exclude/>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>
<!-- ... -->

有关详细信息,请参阅 Maven Surefire 的 测试的包含和排除 文档。

按标签筛选

您可以使用以下配置属性按 标签标签表达式 筛选测试。

  • 要包含标签标签表达式,请使用 groups

  • 要排除标签标签表达式,请使用 excludedGroups

<!-- ... -->
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version>
            <configuration>
                <groups>acceptance | !feature-a</groups>
                <excludedGroups>integration, regression</excludedGroups>
            </configuration>
        </plugin>
    </plugins>
</build>
<!-- ... -->
配置参数

您可以设置 JUnit Platform 配置参数 来影响测试发现和执行,方法是声明 configurationParameters 属性并使用 Java Properties 文件语法(如下所示)或通过 junit-platform.properties 文件提供键值对。

<!-- ... -->
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version>
            <configuration>
                <properties>
                    <configurationParameters>
                        junit.jupiter.conditions.deactivate = *
                        junit.jupiter.extensions.autodetection.enabled = true
                        junit.jupiter.testinstance.lifecycle.default = per_class
                    </configurationParameters>
                </properties>
            </configuration>
        </plugin>
    </plugins>
</build>
<!-- ... -->

4.2.3. Ant

从版本 1.10.3 开始,Ant 具有一个 junitlauncher 任务,该任务提供对在 JUnit Platform 上启动测试的原生支持。junitlauncher 任务仅负责启动 JUnit Platform 并将选定的测试集合传递给它。然后,JUnit Platform 将委托给已注册的测试引擎来发现和执行测试。

junitlauncher 任务尝试尽可能地与原生 Ant 结构(例如 资源集合)保持一致,以允许用户选择他们希望由测试引擎执行的测试。与许多其他核心 Ant 任务相比,这使该任务具有始终如一且自然的体验。

从 Ant 版本 1.10.6 开始,junitlauncher 任务支持 在单独的 JVM 中分叉测试

junit5-jupiter-starter-ant 项目中的 build.xml 文件演示了如何使用该任务,并且可以作为起点。

基本用法

以下示例演示了如何配置 junitlauncher 任务以选择单个测试类(即 org.myapp.test.MyFirstJUnit5Test)。

<path id="test.classpath">
    <!-- The location where you have your compiled classes -->
    <pathelement location="${build.classes.dir}" />
</path>

<!-- ... -->

<junitlauncher>
    <classpath refid="test.classpath" />
    <test name="org.myapp.test.MyFirstJUnit5Test" />
</junitlauncher>

test 元素允许您指定要选择和执行的单个测试类。classpath 元素允许您指定用于启动 JUnit Platform 的类路径。此类路径还将用于查找作为执行一部分的测试类。

以下示例演示了如何配置 junitlauncher 任务以从多个位置选择测试类。

<path id="test.classpath">
    <!-- The location where you have your compiled classes -->
    <pathelement location="${build.classes.dir}" />
</path>
<!-- ... -->
<junitlauncher>
    <classpath refid="test.classpath" />
    <testclasses outputdir="${output.dir}">
        <fileset dir="${build.classes.dir}">
            <include name="org/example/**/demo/**/" />
        </fileset>
        <fileset dir="${some.other.dir}">
            <include name="org/myapp/**/" />
        </fileset>
    </testclasses>
</junitlauncher>

在上面的示例中,testclasses 元素允许您选择位于不同位置的多个测试类。

有关用法和配置选项的更多详细信息,请参阅 junitlauncher 任务 的官方 Ant 文档。

4.2.4. Spring Boot

Spring Boot 提供对管理项目中使用的 JUnit 版本的自动支持。此外,spring-boot-starter-test 工件会自动包含测试库,例如 JUnit Jupiter、AssertJ、Mockito 等。

如果您的构建依赖于 Spring Boot 的依赖项管理支持,则不应在构建脚本中导入 junit-bom,因为这会导致重复(并且可能冲突)的 JUnit 依赖项管理。

如果您需要覆盖 Spring Boot 应用程序中使用的依赖项的版本,则必须覆盖 Spring Boot 插件使用的 BOM 中定义的 版本属性 的确切名称。例如,Spring Boot 中的 JUnit Jupiter 版本属性的名称是 junit-jupiter.version。更改依赖项版本的机制在 GradleMaven 中都有记录。

使用 Gradle,您可以通过在 build.gradle 文件中包含以下内容来覆盖 JUnit Jupiter 版本。

ext['junit-jupiter.version'] = '5.10.2'

使用 Maven,您可以通过在 pom.xml 文件中包含以下内容来覆盖 JUnit Jupiter 版本。

<properties>
    <junit-jupiter.version>5.10.2</junit-jupiter.version>
</properties>

4.3. 控制台启动器

ConsoleLauncher 是一个命令行 Java 应用程序,允许您从控制台启动 JUnit Platform。例如,它可以用于运行 JUnit Vintage 和 JUnit Jupiter 测试并将测试执行结果打印到控制台。

包含所有依赖项的可执行 junit-platform-console-standalone-1.10.2.jar 发布在 Maven Central 存储库的 junit-platform-console-standalone 目录下。它包含以下依赖项

  • junit:junit:4.13.2

  • org.apiguardian:apiguardian-api:1.1.2

  • org.hamcrest:hamcrest-core:1.3

  • org.junit.jupiter:junit-jupiter-api:5.10.2

  • org.junit.jupiter:junit-jupiter-engine:5.10.2

  • org.junit.jupiter:junit-jupiter-params:5.10.2

  • org.junit.platform:junit-platform-commons:1.10.2

  • org.junit.platform:junit-platform-console:1.10.2

  • org.junit.platform:junit-platform-engine:1.10.2

  • org.junit.platform:junit-platform-launcher:1.10.2

  • org.junit.platform:junit-platform-reporting:1.10.2

  • org.junit.platform:junit-platform-suite-api:1.10.2

  • org.junit.platform:junit-platform-suite-commons:1.10.2

  • org.junit.platform:junit-platform-suite-engine:1.10.2

  • org.junit.platform:junit-platform-suite:1.10.2

  • org.junit.vintage:junit-vintage-engine:5.10.2

  • org.opentest4j:opentest4j:1.3.0

您可以 运行 独立的 ConsoleLauncher,如下所示。

$ java -jar junit-platform-console-standalone-1.10.2.jar execute <OPTIONS>

├─ JUnit Vintage
│  └─ example.JUnit4Tests
│     └─ standardJUnit4Test ✔
└─ JUnit Jupiter
   ├─ StandardTests
   │  ├─ succeedingTest() ✔
   │  └─ skippedTest() ↷ for demonstration purposes
   └─ A special test case
      ├─ Custom test name containing spaces ✔
      ├─ ╯°□°)╯ ✔
      └─ 😱 ✔

Test run finished after 64 ms
[         5 containers found      ]
[         0 containers skipped    ]
[         5 containers started    ]
[         0 containers aborted    ]
[         5 containers successful ]
[         0 containers failed     ]
[         6 tests found           ]
[         1 tests skipped         ]
[         5 tests started         ]
[         0 tests aborted         ]
[         5 tests successful      ]
[         0 tests failed          ]

您也可以运行独立的 ConsoleLauncher,如下所示(例如,要包含目录中的所有 jar 文件)

$ java -cp classes:testlib/* org.junit.platform.console.ConsoleLauncher <OPTIONS>
退出代码
如果任何容器或测试失败,ConsoleLauncher 将以状态代码 1 退出。如果未发现任何测试并且提供了 --fail-if-no-tests 命令行选项,则 ConsoleLauncher 将以状态代码 2 退出。否则,退出代码为 0

4.3.1. 子命令和选项

ConsoleLauncher 提供以下子命令

Usage: junit [OPTIONS] [COMMAND]
Launches the JUnit Platform for test discovery and execution.
      [@<filename>...]   One or more argument files containing options.
Commands:
  discover  Discover tests
  execute   Execute tests
  engines   List available test engines

For more information, please refer to the JUnit User Guide at
https://junit.java.net.cn/junit5/docs/current/user-guide/
发现测试
Usage: junit discover [OPTIONS]
Discover tests
      [@<filename>...]       One or more argument files containing options.
      --disable-banner       Disable print out of the welcome message.
      --disable-ansi-colors  Disable ANSI colors in output (not supported by all terminals).
  -h, --help                 Display help information.

SELECTORS

      --scan-classpath, --scan-class-path[=PATH]
                             Scan all directories on the classpath or explicit classpath
                               roots. Without arguments, only directories on the system
                               classpath as well as additional classpath entries supplied via
                               -cp (directories and JAR files) are scanned. Explicit classpath
                               roots that are not on the classpath will be silently ignored.
                               This option can be repeated.
      --scan-modules         Scan all resolved modules for test discovery.
  -u, --select-uri=URI       Select a URI for test discovery. This option can be repeated.
  -f, --select-file=FILE     Select a file for test discovery. This option can be repeated.
  -d, --select-directory=DIR Select a directory for test discovery. This option can be
                               repeated.
  -o, --select-module=NAME   Select single module for test discovery. This option can be
                               repeated.
  -p, --select-package=PKG   Select a package for test discovery. This option can be repeated.
  -c, --select-class=CLASS   Select a class for test discovery. This option can be repeated.
  -m, --select-method=NAME   Select a method for test discovery. This option can be repeated.
  -r, --select-resource=RESOURCE
                             Select a classpath resource for test discovery. This option can
                               be repeated.
  -i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]
                             Select iterations for test discovery (e.g. method:com.acme.Foo#m()
                               [1..2]). This option can be repeated.

FILTERS

  -n, --include-classname=PATTERN
                             Provide a regular expression to include only classes whose fully
                               qualified names match. To avoid loading classes unnecessarily,
                               the default pattern only includes class names that begin with
                               "Test" or end with "Test" or "Tests". When this option is
                               repeated, all patterns will be combined using OR semantics.
                               Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$
  -N, --exclude-classname=PATTERN
                             Provide a regular expression to exclude those classes whose fully
                               qualified names match. When this option is repeated, all
                               patterns will be combined using OR semantics.
      --include-package=PKG  Provide a package to be included in the test run. This option can
                               be repeated.
      --exclude-package=PKG  Provide a package to be excluded from the test run. This option
                               can be repeated.
  -t, --include-tag=TAG      Provide a tag or tag expression to include only tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -T, --exclude-tag=TAG      Provide a tag or tag expression to exclude those tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -e, --include-engine=ID    Provide the ID of an engine to be included in the test run. This
                               option can be repeated.
  -E, --exclude-engine=ID    Provide the ID of an engine to be excluded from the test run.
                               This option can be repeated.

RUNTIME CONFIGURATION

      -cp, --classpath, --class-path=PATH
                             Provide additional classpath entries -- for example, for adding
                               engines and their dependencies. This option can be repeated.
      --config=KEY=VALUE     Set a configuration parameter for test discovery and execution.
                               This option can be repeated.

CONSOLE OUTPUT

      --color-palette=FILE   Specify a path to a properties file to customize ANSI style of
                               output (not supported by all terminals).
      --single-color         Style test output using only text attributes, no color (not
                               supported by all terminals).
      --details=MODE         Select an output details mode for when tests are executed. Use
                               one of: none, summary, flat, tree, verbose, testfeed. If 'none'
                               is selected, then only the summary and test failures are shown.
                               Default: tree.
      --details-theme=THEME  Select an output details tree theme for when tests are executed.
                               Use one of: ascii, unicode. Default is detected based on
                               default character encoding.

For more information, please refer to the JUnit User Guide at
https://junit.java.net.cn/junit5/docs/current/user-guide/
执行测试
Usage: junit execute [OPTIONS]
Execute tests
      [@<filename>...]       One or more argument files containing options.
      --disable-banner       Disable print out of the welcome message.
      --disable-ansi-colors  Disable ANSI colors in output (not supported by all terminals).
  -h, --help                 Display help information.

SELECTORS

      --scan-classpath, --scan-class-path[=PATH]
                             Scan all directories on the classpath or explicit classpath
                               roots. Without arguments, only directories on the system
                               classpath as well as additional classpath entries supplied via
                               -cp (directories and JAR files) are scanned. Explicit classpath
                               roots that are not on the classpath will be silently ignored.
                               This option can be repeated.
      --scan-modules         Scan all resolved modules for test discovery.
  -u, --select-uri=URI       Select a URI for test discovery. This option can be repeated.
  -f, --select-file=FILE     Select a file for test discovery. This option can be repeated.
  -d, --select-directory=DIR Select a directory for test discovery. This option can be
                               repeated.
  -o, --select-module=NAME   Select single module for test discovery. This option can be
                               repeated.
  -p, --select-package=PKG   Select a package for test discovery. This option can be repeated.
  -c, --select-class=CLASS   Select a class for test discovery. This option can be repeated.
  -m, --select-method=NAME   Select a method for test discovery. This option can be repeated.
  -r, --select-resource=RESOURCE
                             Select a classpath resource for test discovery. This option can
                               be repeated.
  -i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]
                             Select iterations for test discovery (e.g. method:com.acme.Foo#m()
                               [1..2]). This option can be repeated.

FILTERS

  -n, --include-classname=PATTERN
                             Provide a regular expression to include only classes whose fully
                               qualified names match. To avoid loading classes unnecessarily,
                               the default pattern only includes class names that begin with
                               "Test" or end with "Test" or "Tests". When this option is
                               repeated, all patterns will be combined using OR semantics.
                               Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$
  -N, --exclude-classname=PATTERN
                             Provide a regular expression to exclude those classes whose fully
                               qualified names match. When this option is repeated, all
                               patterns will be combined using OR semantics.
      --include-package=PKG  Provide a package to be included in the test run. This option can
                               be repeated.
      --exclude-package=PKG  Provide a package to be excluded from the test run. This option
                               can be repeated.
  -t, --include-tag=TAG      Provide a tag or tag expression to include only tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -T, --exclude-tag=TAG      Provide a tag or tag expression to exclude those tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -e, --include-engine=ID    Provide the ID of an engine to be included in the test run. This
                               option can be repeated.
  -E, --exclude-engine=ID    Provide the ID of an engine to be excluded from the test run.
                               This option can be repeated.

RUNTIME CONFIGURATION

      -cp, --classpath, --class-path=PATH
                             Provide additional classpath entries -- for example, for adding
                               engines and their dependencies. This option can be repeated.
      --config=KEY=VALUE     Set a configuration parameter for test discovery and execution.
                               This option can be repeated.

CONSOLE OUTPUT

      --color-palette=FILE   Specify a path to a properties file to customize ANSI style of
                               output (not supported by all terminals).
      --single-color         Style test output using only text attributes, no color (not
                               supported by all terminals).
      --details=MODE         Select an output details mode for when tests are executed. Use
                               one of: none, summary, flat, tree, verbose, testfeed. If 'none'
                               is selected, then only the summary and test failures are shown.
                               Default: tree.
      --details-theme=THEME  Select an output details tree theme for when tests are executed.
                               Use one of: ascii, unicode. Default is detected based on
                               default character encoding.

REPORTING

      --fail-if-no-tests     Fail and return exit status code 2 if no tests are found.
      --reports-dir=DIR      Enable report output into a specified local directory (will be
                               created if it does not exist).

For more information, please refer to the JUnit User Guide at
https://junit.java.net.cn/junit5/docs/current/user-guide/
列出测试引擎
Usage: junit engines [OPTIONS]
List available test engines
      [@<filename>...]   One or more argument files containing options.
      --disable-banner   Disable print out of the welcome message.
      --disable-ansi-colors
                         Disable ANSI colors in output (not supported by all terminals).
  -h, --help             Display help information.

For more information, please refer to the JUnit User Guide at
https://junit.java.net.cn/junit5/docs/current/user-guide/

4.3.2. 参数文件(@-文件)

在某些平台上,当创建包含大量选项或长参数的命令行时,您可能会遇到命令行长度的系统限制。

从版本 1.3 开始,ConsoleLauncher 支持参数文件,也称为@-文件。参数文件是本身包含要传递给命令的参数的文件。当底层的 picocli 命令行解析器遇到以字符 @ 开头的参数时,它会将该文件的内容扩展到参数列表中。

文件中的参数可以用空格或换行符分隔。如果参数包含嵌入的空格,则整个参数应包含在双引号或单引号中,例如 "-f=My Files/Stuff.java"

如果参数文件不存在或无法读取,则该参数将被视为字面量,并且不会被删除。这很可能会导致“不匹配的参数”错误消息。您可以通过使用 picocli.trace 系统属性设置为 DEBUG 来执行命令来排查此类错误。

可以在命令行上指定多个@-文件。指定的路径可以相对于当前目录或绝对路径。

您可以通过使用额外的 @ 符号转义来传递以初始 @ 字符开头的实际参数。例如,@@somearg 将变为 @somearg,并且不会被扩展。

4.3.3. 颜色自定义

ConsoleLauncher 输出中使用的颜色可以自定义。选项 --single-color 将应用内置单色样式,而 --color-palette 将接受一个属性文件来覆盖 ANSI SGR 颜色样式。以下属性文件演示了默认样式

SUCCESSFUL = 32
ABORTED = 33
FAILED = 31
SKIPPED = 35
CONTAINER = 35
TEST = 34
DYNAMIC = 35
REPORTED = 37

4.4. 使用 JUnit 4 运行 JUnit Platform

JUnitPlatform 运行器已弃用

JUnitPlatform 运行器是由 JUnit 团队开发的,作为在 JUnit 4 环境中在 JUnit Platform 上运行测试套件和测试的临时解决方案。

近年来,所有主流构建工具和 IDE 都提供对直接在 JUnit Platform 上运行测试的内置支持。

此外,junit-platform-suite-engine 模块提供的 @Suite 支持的引入使 JUnitPlatform 运行器过时。有关详细信息,请参阅 JUnit Platform Suite Engine

因此,JUnitPlatform 运行器和 @UseTechnicalNames 注解已在 JUnit Platform 1.8 中弃用,并将从 JUnit Platform 2.0 中删除。

如果您正在使用 JUnitPlatform 运行器,请迁移到 @Suite 支持。

JUnitPlatform 运行器是一个基于 JUnit 4 的 Runner,它使您能够在 JUnit 4 环境中运行任何其编程模型在 JUnit Platform 上受支持的测试,例如 JUnit Jupiter 测试类。

使用 `@RunWith(JUnitPlatform.class)` 注解类,可以让它在支持 JUnit 4 但尚未直接支持 JUnit Platform 的 IDE 和构建系统中运行。

由于 JUnit Platform 具有 JUnit 4 不具备的功能,因此该运行器只能支持 JUnit Platform 功能的一个子集,尤其是在报告方面(参见 显示名称与技术名称)。

4.4.1. 设置

您需要在类路径中包含以下工件及其依赖项。有关组 ID、工件 ID 和版本的详细信息,请参见 依赖项元数据

显式依赖项
  • 测试范围内的 `junit-platform-runner`:`JUnitPlatform` 运行器的位置

  • 测试范围内的 `junit-4.13.2.jar`:用于使用 JUnit 4 运行测试

  • 测试范围内的 `junit-jupiter-api`:使用 JUnit Jupiter 编写测试的 API,包括 `@Test` 等。

  • 测试运行时范围内的 `junit-jupiter-engine`:`TestEngine` API 的实现,用于 JUnit Jupiter

传递依赖项
  • 测试范围内的 `junit-platform-suite-api`

  • 测试范围内的 `junit-platform-suite-commons`

  • 测试范围内的 `junit-platform-launcher`

  • 测试范围内的 `junit-platform-engine`

  • 测试范围内的 `junit-platform-commons`

  • 测试范围内的 `opentest4j`

4.4.2. 显示名称与技术名称

要为通过 `@RunWith(JUnitPlatform.class)` 注解运行的类定义自定义显示名称,请使用 `@SuiteDisplayName` 注解该类并提供自定义值。

默认情况下,显示名称将用于测试工件;但是,当使用 `JUnitPlatform` 运行器使用 Gradle 或 Maven 等构建工具执行测试时,生成的测试报告通常需要包含测试工件的技术名称(例如,完全限定的类名),而不是更短的显示名称,例如测试类的简单名称或包含特殊字符的自定义显示名称。要为报告目的启用技术名称,请在 `@RunWith(JUnitPlatform.class)` 旁边声明 `@UseTechnicalNames` 注解。

请注意,`@UseTechnicalNames` 的存在会覆盖通过 `@SuiteDisplayName` 配置的任何自定义显示名称。

4.4.3. 单个测试类

使用 `JUnitPlatform` 运行器的一种方法是直接使用 `@RunWith(JUnitPlatform.class)` 注解测试类。请注意,以下示例中的测试方法使用 `org.junit.jupiter.api.Test`(JUnit Jupiter)注解,而不是 `org.junit.Test`(JUnit 4)注解。此外,在这种情况下,测试类必须是 `public`;否则,某些 IDE 和构建工具可能无法将其识别为 JUnit 4 测试类。

import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;

@RunWith(org.junit.platform.runner.JUnitPlatform.class)
public class JUnitPlatformClassDemo {

    @Test
    void succeedingTest() {
        /* no-op */
    }

    @Test
    void failingTest() {
        fail("Failing for failing's sake.");
    }

}

4.4.4. 测试套件

如果您有多个测试类,您可以创建测试套件,如以下示例所示。

import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.runner.RunWith;

@RunWith(org.junit.platform.runner.JUnitPlatform.class)
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
public class JUnitPlatformSuiteDemo {
}

`JUnitPlatformSuiteDemo` 将发现并运行 `example` 包及其子包中的所有测试。默认情况下,它只包含名称以 `Test` 开头或以 `Test` 或 `Tests` 结尾的测试类。

其他配置选项
除了 `@SelectPackages` 之外,还有更多用于发现和过滤测试的配置选项。有关更多详细信息,请参阅 `org.junit.platform.suite.api` 包的 Javadoc。
使用 `@RunWith(JUnitPlatform.class)` 注解的测试类和套件**不能**直接在 JUnit Platform 上执行(或作为某些 IDE 中记录的“JUnit 5”测试)。此类类和套件只能使用 JUnit 4 基础设施执行。

4.5. 配置参数

除了指示平台要包含哪些测试类和测试引擎、要扫描哪些包等之外,有时还需要提供特定于特定测试引擎、监听器或注册扩展的附加自定义配置参数。例如,JUnit Jupiter `TestEngine` 支持以下用例的配置参数

配置参数是基于文本的键值对,可以通过以下机制之一提供给在 JUnit Platform 上运行的测试引擎。

  1. `LauncherDiscoveryRequestBuilder` 中的 `configurationParameter()` 和 `configurationParameters()` 方法,用于构建提供给 `Launcher` API 的请求。当通过 JUnit Platform 提供的工具之一运行测试时,您可以按如下方式指定配置参数

  2. JVM 系统属性。

  3. JUnit Platform 配置文件:类路径根目录中的名为 `junit-platform.properties` 的文件,遵循 Java `Properties` 文件的语法规则。

配置参数按上述定义的顺序查找。因此,直接提供给 `Launcher` 的配置参数优先于通过系统属性和配置文件提供的配置参数。类似地,通过系统属性提供的配置参数优先于通过配置文件提供的配置参数。

4.5.1. 模式匹配语法

本节介绍应用于以下功能使用的配置参数的模式匹配语法。

如果给定配置参数的值仅包含一个星号 (*),则该模式将匹配所有候选类。否则,该值将被视为一个逗号分隔的模式列表,其中每个模式将与每个候选类的完全限定类名 (FQCN) 匹配。模式中的任何点 (.) 将匹配 FQCN 中的点 (.) 或美元符号 ($)。任何星号 (*) 将匹配 FQCN 中的一个或多个字符。模式中的所有其他字符将一对一地与 FQCN 匹配。

示例

  • *:匹配所有候选类。

  • org.junit.*:匹配 `org.junit` 基本包及其任何子包下的所有候选类。

  • *.MyCustomImpl:匹配每个简单类名恰好为 `MyCustomImpl` 的候选类。

  • *System*:匹配每个 FQCN 包含 `System` 的候选类。

  • *System*, *Unit*:匹配每个 FQCN 包含 `System` 或 `Unit` 的候选类。

  • org.example.MyCustomImpl:匹配 FQCN 恰好为 `org.example.MyCustomImpl` 的候选类。

  • org.example.MyCustomImpl, org.example.TheirCustomImpl:匹配 FQCN 恰好为 `org.example.MyCustomImpl` 或 `org.example.TheirCustomImpl` 的候选类。

4.6. 标签

标签是 JUnit Platform 用于标记和过滤测试的概念。向容器和测试添加标签的编程模型由测试框架定义。例如,在基于 JUnit Jupiter 的测试中,应使用 `@Tag` 注解(参见 标记和过滤)。对于基于 JUnit 4 的测试,Vintage 引擎将 `@Category` 注解映射到标签(参见 类别支持)。其他测试框架可能会定义自己的注解或其他方法供用户指定标签。

4.6.1. 标签语法规则

无论如何指定标签,JUnit Platform 都强制执行以下规则

  • 标签不能为 `null` 或空白

  • 修剪后的标签不能包含空格。

  • 修剪后的标签不能包含 ISO 控制字符。

  • 修剪后的标签不能包含以下任何保留字符

    • ,逗号

    • (左括号

    • )右括号

    • &与号

    • |竖线

    • !感叹号

在上述上下文中,“修剪”表示已删除前导和尾随空格字符。

4.6.2. 标签表达式

标签表达式是使用运算符 `!`、`&` 和 `|` 的布尔表达式。此外,可以使用 `( `和 `) `来调整运算符优先级。

支持两种特殊表达式,`any()` 和 `none()`,它们分别选择所有具有任何标签的测试和所有没有任何标签的测试。这些特殊表达式可以像普通标签一样与其他表达式组合。

表 2. 运算符(按优先级降序排列)
运算符 含义 结合性

!

not

&

and

|

or

如果您在多个维度上标记测试,标签表达式可以帮助您选择要执行的测试。当按测试类型(例如,微型集成端到端)和功能(例如,产品目录运输)进行标记时,以下标签表达式可能会有用。 标签表达式

选择

product

产品的所有测试

catalog | shipping

目录的所有测试加上运输的所有测试

catalog & shipping

目录运输的交集的所有测试

product & !end-to-end

产品的所有测试,但不是端到端测试

(micro | integration) & (product | shipping)

产品运输的所有微型集成测试

4.7. 捕获标准输出/错误

如果启用,JUnit Platform 会捕获相应的输出,并使用stdoutstderr键将其发布为报告条目,以便在报告测试或容器已完成之前立即将其发布到所有已注册的TestExecutionListener实例。

请注意,捕获的输出将仅包含由用于执行容器或测试的线程发出的输出。其他线程的任何输出都将被省略,因为特别是在并行执行测试时,不可能将其归因于特定测试或容器。

4.8. 使用监听器和拦截器

JUnit Platform 提供以下监听器 API,允许 JUnit、第三方和自定义用户代码对在TestPlan的发现和执行期间的不同点触发的事件做出反应。

LauncherSessionListener API 通常由构建工具或 IDE 实现,并自动为您注册,以支持构建工具或 IDE 的某些功能。

LauncherDiscoveryListenerTestExecutionListener API 通常是为了生成某种形式的报告或在 IDE 中显示测试计划的图形表示而实现的。此类监听器可以由构建工具或 IDE 实现并自动注册,也可以包含在第三方库中,并可能自动为您注册。您也可以实现和注册自己的监听器。

有关注册和配置监听器的详细信息,请参阅本指南的以下部分。

JUnit Platform 提供以下监听器,您可能希望将其与您的测试套件一起使用。

JUnit Platform 报告

LegacyXmlReportGeneratingListener可以通过控制台启动器使用,也可以手动注册以生成与基于 JUnit 4 的测试报告的事实标准兼容的 XML 报告。

OpenTestReportGeneratingListener根据Open Test Reporting中指定的基于事件的格式生成 XML 报告。它会自动注册,并且可以通过配置参数启用和配置。

有关详细信息,请参阅JUnit Platform 报告

飞行记录器支持

FlightRecordingExecutionListenerFlightRecordingDiscoveryListener在测试发现和执行期间生成 Java Flight Recorder 事件。

LoggingListener

TestExecutionListener用于通过BiConsumer记录所有事件的信息消息,该BiConsumer使用ThrowableSupplier<String>

SummaryGeneratingListener

TestExecutionListener,它生成测试执行的摘要,可以通过PrintWriter打印。

UniqueIdTrackingListener

TestExecutionListener,它跟踪在TestPlan执行期间跳过或执行的所有测试的唯一 ID,并在TestPlan执行完成后生成包含唯一 ID 的文件。

4.8.1. 飞行记录器支持

从 1.7 版本开始,JUnit Platform 提供了对生成飞行记录器事件的可选支持。JEP 328将 Java Flight Recorder (JFR) 描述为

飞行记录器记录来自应用程序、JVM 和操作系统的事件。事件存储在一个文件中,可以附加到错误报告中并由支持工程师检查,从而允许在问题发生之前对问题进行事后分析。

为了记录在运行测试时生成的飞行记录器事件,您需要

  1. 确保您使用的是 Java 8 Update 262 或更高版本,或者 Java 11 或更高版本。

  2. 在测试运行时将org.junit.platform.jfr模块(junit-platform-jfr-1.10.2.jar)提供在类路径或模块路径上。

  3. 在启动测试运行时启动飞行记录。飞行记录器可以通过 java 命令行选项启动

    -XX:StartFlightRecording:filename=...

请查阅您的构建工具手册以获取相应的命令。

要分析记录的事件,请使用最近 JDK 附带的jfr命令行工具,或使用JDK Mission Control打开记录文件。

飞行记录器支持目前是一个实验性功能。欢迎您试用并向 JUnit 团队提供反馈,以便他们可以改进并最终推广此功能。

4.9. 堆栈跟踪修剪

从 1.10 版本开始,JUnit Platform 提供了对修剪失败测试产生的堆栈跟踪的内置支持。此功能默认启用,但可以通过将junit.platform.stacktrace.pruning.enabled配置参数设置为false来禁用。

启用后,将从堆栈跟踪中删除来自org.junitjdk.internal.reflectsun.reflect包的所有调用,除非这些调用发生在测试本身或其任何祖先之后。因此,对org.junit.jupiter.api.Assertionsorg.junit.jupiter.api.Assumptions的调用将永远不会被排除。

此外,将删除在第一个来自 JUnit Platform Launcher 的调用之前和包括该调用的所有元素。

5. 扩展模型

5.1. 概述

与 JUnit 4 中竞争的RunnerTestRuleMethodRule扩展点相比,JUnit Jupiter 扩展模型包含一个单一、连贯的概念:Extension API。但是,请注意,Extension本身只是一个标记接口。

5.2. 注册扩展

扩展可以通过@ExtendWith声明式注册,可以通过@RegisterExtension以编程方式注册,或者可以通过 Java 的ServiceLoader机制自动注册。

5.2.1. 声明式扩展注册

开发人员可以通过使用@ExtendWith(…​)注释测试接口、测试类、测试方法或自定义组合注释,并为要注册的扩展提供类引用,来声明式注册一个或多个扩展。从 JUnit Jupiter 5.8 开始,@ExtendWith也可以在测试类构造函数中的字段或参数上,在测试方法中,以及在@BeforeAll@AfterAll@BeforeEach@AfterEach生命周期方法中声明。

例如,要为特定测试方法注册WebServerExtension,您需要按如下方式注释测试方法。我们假设WebServerExtension启动一个本地 Web 服务器,并将服务器的 URL 注入到用@WebServerUrl注释的参数中。

@Test
@ExtendWith(WebServerExtension.class)
void getProductList(@WebServerUrl String serverUrl) {
    WebClient webClient = new WebClient();
    // Use WebClient to connect to web server using serverUrl and verify response
    assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}

要为特定类及其子类中的所有测试注册WebServerExtension,您需要按如下方式注释测试类。

@ExtendWith(WebServerExtension.class)
class MyTests {
    // ...
}

可以像这样一起注册多个扩展

@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
class MyFirstTests {
    // ...
}

或者,可以像这样分别注册多个扩展

@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
class MySecondTests {
    // ...
}
扩展注册顺序

在类级别、方法级别或参数级别通过@ExtendWith声明式注册的扩展将按其在源代码中声明的顺序执行。例如,MyFirstTestsMySecondTests中测试的执行将由DatabaseExtensionWebServerExtension扩展,完全按此顺序

如果您希望以可重用方式组合多个扩展,则可以定义一个自定义组合注释,并在以下代码清单中使用@ExtendWith作为元注释。然后,@DatabaseAndWebServerExtension可以代替@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })使用。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
public @interface DatabaseAndWebServerExtension {
}

以上示例演示了如何在类级别或方法级别应用@ExtendWith;但是,对于某些用例,扩展在字段或参数级别声明式注册是有意义的。考虑一个RandomNumberExtension,它生成随机数,可以将其注入字段,或者通过构造函数、测试方法或生命周期方法中的参数注入。如果扩展提供了一个用@ExtendWith(RandomNumberExtension.class)(见下文清单)元注释的@Random注释,则扩展可以像以下RandomNumberDemo示例中那样透明地使用。

@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {

    // Use static randomNumber0 field anywhere in the test class,
    // including @BeforeAll or @AfterEach lifecycle methods.
    @Random
    private static Integer randomNumber0;

    // Use randomNumber1 field in test methods and @BeforeEach
    // or @AfterEach lifecycle methods.
    @Random
    private int randomNumber1;

    RandomNumberDemo(@Random int randomNumber2) {
        // Use randomNumber2 in constructor.
    }

    @BeforeEach
    void beforeEach(@Random int randomNumber3) {
        // Use randomNumber3 in @BeforeEach method.
    }

    @Test
    void test(@Random int randomNumber4) {
        // Use randomNumber4 in test method.
    }

}

以下代码清单提供了一个关于如何实现RandomNumberExtension的示例。此实现适用于RandomNumberDemo中的用例;但是,它可能不足以涵盖所有用例,例如,随机数生成支持仅限于整数;它使用java.util.Random而不是java.security.SecureRandom;等等。无论如何,重要的是要注意实现了哪些扩展 API 以及原因。

具体来说,RandomNumberExtension实现了以下扩展 API

  • BeforeAllCallback:支持静态字段注入

  • BeforeEachCallback:支持非静态字段注入

  • ParameterResolver:支持构造函数和方法注入

理想情况下,RandomNumberExtension将实现TestInstancePostProcessor而不是BeforeEachCallback,以便在测试类实例化后立即支持非静态字段注入。

但是,JUnit Jupiter 目前不允许通过@ExtendWith在非静态字段上注册TestInstancePostProcessor(见问题 3437)。鉴于此,RandomNumberExtension实现BeforeEachCallback作为一种替代方法。

import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields;

import java.lang.reflect.Field;
import java.util.function.Predicate;

import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.ModifierSupport;

class RandomNumberExtension
        implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {

    private final java.util.Random random = new java.util.Random(System.nanoTime());

    /**
     * Inject a random integer into static fields that are annotated with
     * {@code @Random} and can be assigned an integer value.
     */
    @Override
    public void beforeAll(ExtensionContext context) {
        Class<?> testClass = context.getRequiredTestClass();
        injectFields(testClass, null, ModifierSupport::isStatic);
    }

    /**
     * Inject a random integer into non-static fields that are annotated with
     * {@code @Random} and can be assigned an integer value.
     */
    @Override
    public void beforeEach(ExtensionContext context) {
        Class<?> testClass = context.getRequiredTestClass();
        Object testInstance = context.getRequiredTestInstance();
        injectFields(testClass, testInstance, ModifierSupport::isNotStatic);
    }

    /**
     * Determine if the parameter is annotated with {@code @Random} and can be
     * assigned an integer value.
     */
    @Override
    public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
        return pc.isAnnotated(Random.class) && isInteger(pc.getParameter().getType());
    }

    /**
     * Resolve a random integer.
     */
    @Override
    public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) {
        return this.random.nextInt();
    }

    private void injectFields(Class<?> testClass, Object testInstance,
            Predicate<Field> predicate) {

        predicate = predicate.and(field -> isInteger(field.getType()));
        findAnnotatedFields(testClass, Random.class, predicate)
            .forEach(field -> {
                try {
                    field.setAccessible(true);
                    field.set(testInstance, this.random.nextInt());
                }
                catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            });
    }

    private static boolean isInteger(Class<?> type) {
        return type == Integer.class || type == int.class;
    }

}
@ExtendWith在字段上的扩展注册顺序

通过@ExtendWith在字段上声明式注册的扩展将相对于@RegisterExtension字段和其他@ExtendWith字段进行排序,使用的是确定性但有意不明显的算法。但是,可以使用@Order注释对@ExtendWith字段进行排序。有关详细信息,请参阅@RegisterExtension字段的扩展注册顺序提示。

@ExtendWith字段可以是static或非静态的。关于@RegisterExtension字段的静态字段实例字段的文档也适用于@ExtendWith字段。

5.2.2. 以编程方式注册扩展

开发人员可以通过在测试类中的字段上使用@RegisterExtension注释来以编程方式注册扩展。

当通过@ExtendWith声明式注册扩展时,通常只能通过注释对其进行配置。相反,当通过@RegisterExtension注册扩展时,可以以编程方式对其进行配置,例如,为了向扩展的构造函数、静态工厂方法或构建器 API 传递参数。

扩展注册顺序

默认情况下,通过@RegisterExtension以编程方式注册的扩展或通过@ExtendWith在字段上声明式注册的扩展将使用确定性但有意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行扩展,从而允许可重复的构建。但是,有时需要以显式顺序注册扩展。为此,请使用@Order注释@RegisterExtension字段或@ExtendWith字段。

任何未用@Order注释的@RegisterExtension字段或@ExtendWith字段将使用默认顺序进行排序,该顺序的值为Integer.MAX_VALUE / 2。这允许使用@Order注释的扩展字段在未注释的扩展字段之前或之后显式排序。具有小于默认顺序值的显式顺序值的扩展将在未注释的扩展之前注册。类似地,具有大于默认顺序值的显式顺序值的扩展将在未注释的扩展之后注册。例如,为扩展分配一个大于默认顺序值的显式顺序值允许before回调扩展最后注册,而after回调扩展首先注册,相对于其他以编程方式注册的扩展。

@RegisterExtension字段不能为null(在评估时),但可以是static或非静态的。
静态字段

如果@RegisterExtension字段是static,则该扩展将在通过@ExtendWith在类级别注册的扩展之后注册。这种静态扩展不受其可以实现的扩展 API 的限制。因此,通过静态字段注册的扩展可以实现类级别和实例级别的扩展 API,例如BeforeAllCallbackAfterAllCallbackTestInstancePostProcessorTestInstancePreDestroyCallback,以及方法级别的扩展 API,例如BeforeEachCallback等。

在以下示例中,测试类中的server字段通过使用WebServerExtension支持的构建器模式以编程方式初始化。配置后的WebServerExtension将自动注册为类级别的扩展,例如,为了在类中的所有测试之前启动服务器,然后在类中的所有测试完成后停止服务器。此外,使用@BeforeAll@AfterAll注释的静态生命周期方法以及@BeforeEach@AfterEach@Test方法可以根据需要通过server字段访问扩展的实例。

在 Java 中通过静态字段注册扩展
class WebServerDemo {

    @RegisterExtension
    static WebServerExtension server = WebServerExtension.builder()
        .enableSecurity(false)
        .build();

    @Test
    void getProductList() {
        WebClient webClient = new WebClient();
        String serverUrl = server.getServerUrl();
        // Use WebClient to connect to web server using serverUrl and verify response
        assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
    }

}
Kotlin 中的静态字段

Kotlin 编程语言没有static字段的概念。但是,可以使用 Kotlin 中的@JvmStatic注释指示编译器生成一个private static字段。如果希望 Kotlin 编译器生成一个public static字段,可以使用@JvmField注释。

以下示例是上一节中WebServerDemo的版本,已移植到 Kotlin。

在 Kotlin 中通过静态字段注册扩展
class KotlinWebServerDemo {

    companion object {
        @JvmStatic
        @RegisterExtension
        val server = WebServerExtension.builder()
            .enableSecurity(false)
            .build()
    }

    @Test
    fun getProductList() {
        // Use WebClient to connect to web server using serverUrl and verify response
        val webClient = WebClient()
        val serverUrl = server.serverUrl
        assertEquals(200, webClient.get("$serverUrl/products").responseStatus)
    }
}
实例字段

如果@RegisterExtension字段是非静态的(即实例字段),则该扩展将在测试类实例化后以及每个注册的TestInstancePostProcessor有机会对测试实例进行后处理(可能将要使用的扩展实例注入到注释字段中)之后注册。因此,如果这种实例扩展实现类级别或实例级别的扩展 API,例如BeforeAllCallbackAfterAllCallbackTestInstancePostProcessor,则不会遵守这些 API。默认情况下,实例扩展将在通过@ExtendWith在方法级别注册的扩展之后注册;但是,如果测试类配置了@TestInstance(Lifecycle.PER_CLASS)语义,则实例扩展将在通过@ExtendWith在方法级别注册的扩展之前注册。

在以下示例中,测试类中的docs字段通过调用自定义lookUpDocsDir()方法并将结果提供给DocumentationExtension中的静态forPath()工厂方法以编程方式初始化。配置后的DocumentationExtension将自动注册为方法级别的扩展。此外,@BeforeEach@AfterEach@Test方法可以根据需要通过docs字段访问扩展的实例。

通过实例字段注册的扩展
class DocumentationDemo {

    static Path lookUpDocsDir() {
        // return path to docs dir
    }

    @RegisterExtension
    DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());

    @Test
    void generateDocumentation() {
        // use this.docs ...
    }
}

5.2.3. 自动扩展注册

除了使用注释的声明式扩展注册以编程方式注册扩展支持之外,JUnit Jupiter 还通过 Java 的ServiceLoader机制支持全局扩展注册,允许根据类路径中可用的内容自动检测和自动注册第三方扩展。

具体来说,可以通过在包含 JAR 文件的/META-INF/services文件夹中名为org.junit.jupiter.api.extension.Extension的文件中提供其完全限定的类名来注册自定义扩展。

启用自动扩展检测

自动检测是一项高级功能,因此默认情况下未启用。要启用它,请将junit.jupiter.extensions.autodetection.enabled配置参数设置为true。这可以作为 JVM 系统属性提供,作为传递给LauncherLauncherDiscoveryRequest中的配置参数,或者通过 JUnit Platform 配置文件提供(有关详细信息,请参阅配置参数)。

例如,要启用扩展的自动检测,可以使用以下系统属性启动 JVM。

-Djunit.jupiter.extensions.autodetection.enabled=true

启用自动检测后,通过ServiceLoader机制发现的扩展将在 JUnit Jupiter 的全局扩展(例如,对TestInfoTestReporter等的支持)之后添加到扩展注册表中。

5.2.4. 扩展继承

注册的扩展在测试类层次结构中以自上而下的语义继承。类似地,在类级别注册的扩展在方法级别继承。此外,特定扩展实现只能为给定扩展上下文及其父上下文注册一次。因此,任何尝试注册重复扩展实现的操作都将被忽略。

5.3. 条件测试执行

ExecutionCondition定义了用于以编程方式进行条件测试执行Extension API。

ExecutionCondition针对每个容器(例如,测试类)进行评估,以确定根据提供的ExtensionContext是否应执行其包含的所有测试。类似地,ExecutionCondition针对每个测试进行评估,以确定根据提供的ExtensionContext是否应执行给定的测试方法。

当注册多个ExecutionCondition扩展时,只要其中一个条件返回disabled,容器或测试就会被禁用。因此,不能保证评估条件,因为另一个扩展可能已经导致容器或测试被禁用。换句话说,评估的工作原理类似于短路布尔 OR 运算符。

有关具体示例,请参阅DisabledCondition@Disabled的源代码。

5.3.1. 禁用条件

有时,在没有某些条件处于活动状态的情况下运行测试套件可能很有用。例如,您可能希望即使测试使用@Disabled注释也运行测试,以查看它们是否仍然损坏。为此,请为junit.jupiter.conditions.deactivate配置参数提供一个模式,以指定应为当前测试运行禁用的(即不评估的)条件。该模式可以作为 JVM 系统属性提供,作为传递给LauncherLauncherDiscoveryRequest中的配置参数,或者通过 JUnit Platform 配置文件提供(有关详细信息,请参阅配置参数)。

例如,要禁用 JUnit 的@Disabled条件,可以使用以下系统属性启动 JVM。

-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition

模式匹配语法

有关详细信息,请参阅模式匹配语法

5.4. 测试实例预构造回调

TestInstancePreConstructCallback定义了希望在构造测试实例(通过构造函数调用或通过TestInstanceFactory之前调用的Extensions的 API。

此扩展提供了对TestInstancePreDestroyCallback的对称调用,并且与其他扩展结合使用时非常有用,可以准备构造函数参数或跟踪测试实例及其生命周期。

5.5. 测试实例工厂

TestInstanceFactory定义了希望创建测试类实例的Extensions的 API。

常见用例包括从依赖注入框架获取测试实例或调用静态工厂方法来创建测试类实例。

如果没有注册TestInstanceFactory,框架将调用测试类的唯一构造函数来实例化它,可能通过注册的ParameterResolver扩展解析构造函数参数。

实现 TestInstanceFactory 的扩展可以在测试接口、顶级测试类或 @Nested 测试类上注册。

为任何单个类注册多个实现 TestInstanceFactory 的扩展将导致在该类、任何子类和任何嵌套类中的所有测试都抛出异常。请注意,在超类或封闭类(例如,在 @Nested 测试类的情况下)中注册的任何 TestInstanceFactory 都将被继承。用户有责任确保只为任何特定测试类注册一个 TestInstanceFactory

5.6. 测试实例后处理

TestInstancePostProcessor 定义了 Extensions 的 API,这些 Extensions 希望后处理测试实例。

常见用例包括将依赖项注入测试实例、在测试实例上调用自定义初始化方法等。

有关具体示例,请参阅 MockitoExtensionSpringExtension 的源代码。

5.7. 测试实例预销毁回调

TestInstancePreDestroyCallback 定义了 Extensions 的 API,这些 Extensions 希望在测试实例在测试中使用且在销毁之前处理测试实例。

常见用例包括清理已注入测试实例的依赖项、在测试实例上调用自定义反初始化方法等。

5.8. 参数解析

ParameterResolver 定义了 Extension 的 API,用于在运行时动态解析参数。

如果测试类构造函数、测试方法生命周期方法(参见 定义)声明了一个参数,则该参数必须在运行时由 ParameterResolver 进行解析ParameterResolver 可以是内置的(参见 TestInfoParameterResolver)或由用户注册。一般来说,参数可以通过名称类型注释或它们的任何组合来解析。

如果您希望实现一个自定义 ParameterResolver,该自定义 ParameterResolver 仅根据参数的类型解析参数,您可能会发现扩展 TypeBasedParameterResolver 很方便,它充当此类用例的通用适配器。

由于 JDK 9 之前的 JDK 版本上的 javac 生成的字节码中的错误,直接通过核心 java.lang.reflect.Parameter API 在参数上查找注释对于内部类构造函数(例如,@Nested 测试类中的构造函数)将始终失败。

因此,提供给 ParameterResolver 实现的 ParameterContext API 包含以下用于在参数上正确查找注释的便捷方法。强烈建议扩展作者使用这些方法,而不是 java.lang.reflect.Parameter 中提供的方法,以避免此 JDK 中的错误。

  • boolean isAnnotated(Class<? extends Annotation> annotationType)

  • Optional<A> findAnnotation(Class<A> annotationType)

  • List<A> findRepeatableAnnotations(Class<A> annotationType)

其他扩展也可以利用注册的 ParameterResolvers 来进行方法和构造函数调用,使用 ExecutableInvoker(可通过 ExtensionContext 中的 getExecutableInvoker() 方法获得)。

5.9. 测试结果处理

TestWatcher 定义了 Extensions 的 API,这些 Extensions 希望处理测试方法执行的结果。具体来说,TestWatcher 将使用以下事件的上下文信息进行调用。

  • testDisabled:在禁用的测试方法被跳过之后调用

  • testSuccessful:在测试方法成功完成之后调用

  • testAborted:在测试方法被中止之后调用

  • testFailed:在测试方法失败之后调用

定义 中介绍的“测试方法”定义相反,在此上下文中,测试方法是指任何 @Test 方法或 @TestTemplate 方法(例如,@RepeatedTest@ParameterizedTest)。

实现此接口的扩展可以在类级别、实例级别或方法级别注册。当在类级别注册时,TestWatcher 将为任何包含的测试方法(包括 @Nested 类中的那些方法)调用。当在方法级别注册时,TestWatcher 将仅为注册了它的测试方法调用。

如果 TestWatcher 是通过非静态(实例)字段注册的(例如,使用 @RegisterExtension),并且测试类配置了 @TestInstance(Lifecycle.PER_METHOD) 语义(这是默认的生命周期模式),则 TestWatcher 不会使用 @TestTemplate 方法(例如,@RepeatedTest@ParameterizedTest)的事件进行调用。

为了确保 TestWatcher 为给定类中的所有测试方法调用,因此建议使用 @ExtendWith 在类级别注册 TestWatcher,或者使用 @RegisterExtension@ExtendWith 通过 static 字段注册。

如果在类级别发生错误(例如,@BeforeAll 方法抛出的异常),则不会报告任何测试结果。类似地,如果测试类通过 ExecutionCondition 被禁用(例如,@Disabled),则不会报告任何测试结果。

与其他 Extension API 相反,TestWatcher 不允许对测试执行产生负面影响。因此,TestWatcher API 中的方法抛出的任何异常都将在 WARNING 级别记录,并且不允许传播或导致测试执行失败。

TestWatcher API 中的方法被调用之前,将关闭存储在提供的 ExtensionContextStore 中的任何 ExtensionContext.Store.CloseableResource 实例(参见 在扩展中保持状态)。您可以使用父上下文的 Store 来处理此类资源。

5.10. 测试生命周期回调

以下接口定义了在测试执行生命周期的各个点扩展测试的 API。有关示例,请参阅以下部分,并参阅 org.junit.jupiter.api.extension 包中每个接口的 Javadoc 以获取更多详细信息。

实现多个扩展 API
扩展开发人员可以选择在一个扩展中实现任意数量的这些接口。有关具体示例,请参阅 SpringExtension 的源代码。

5.10.1. 测试执行前后回调

BeforeTestExecutionCallbackAfterTestExecutionCallback 定义了 Extensions 的 API,这些 Extensions 希望添加将在测试方法执行之前之后立即执行的行为。因此,这些回调非常适合计时、跟踪和类似用例。如果您需要实现围绕 @BeforeEach@AfterEach 方法调用的回调,请改为实现 BeforeEachCallbackAfterEachCallback

以下示例展示了如何使用这些回调来计算和记录测试方法的执行时间。TimingExtension 同时实现了 BeforeTestExecutionCallbackAfterTestExecutionCallback,以便计时和记录测试执行。

计时和记录测试方法执行的扩展
import java.lang.reflect.Method;
import java.util.logging.Logger;

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final Logger logger = Logger.getLogger(TimingExtension.class.getName());

    private static final String START_TIME = "start time";

    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {
        getStore(context).put(START_TIME, System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        Method testMethod = context.getRequiredTestMethod();
        long startTime = getStore(context).remove(START_TIME, long.class);
        long duration = System.currentTimeMillis() - startTime;

        logger.info(() ->
            String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
    }

    private Store getStore(ExtensionContext context) {
        return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
    }

}

由于 TimingExtensionTests 类通过 @ExtendWith 注册了 TimingExtension,因此它的测试将在执行时应用此计时。

使用示例 TimingExtension 的测试类
@ExtendWith(TimingExtension.class)
class TimingExtensionTests {

    @Test
    void sleep20ms() throws Exception {
        Thread.sleep(20);
    }

    @Test
    void sleep50ms() throws Exception {
        Thread.sleep(50);
    }

}

以下是运行 TimingExtensionTests 时产生的日志示例。

INFO: Method [sleep20ms] took 24 ms.
INFO: Method [sleep50ms] took 53 ms.

5.11. 异常处理

在测试执行期间抛出的异常可能会被拦截并相应地处理,然后再进一步传播,以便可以在专门的 Extensions 中定义某些操作,例如错误日志记录或资源释放。JUnit Jupiter 提供了 Extensions 的 API,这些 Extensions 希望通过 TestExecutionExceptionHandler 处理在 @Test 方法期间抛出的异常,以及通过 LifecycleMethodExecutionExceptionHandler 处理在测试生命周期方法(@BeforeAll@BeforeEach@AfterEach@AfterAll)期间抛出的异常。

以下示例展示了一个扩展,它将吞并所有 IOException 实例,但会重新抛出任何其他类型的异常。

在测试执行中过滤 IOException 的异常处理扩展
public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {

    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
            throws Throwable {

        if (throwable instanceof IOException) {
            return;
        }
        throw throwable;
    }
}

另一个示例展示了如何在设置和清理期间抛出意外异常时,准确记录被测应用程序的状态。请注意,与依赖生命周期回调(可能根据测试状态执行或不执行)不同,此解决方案保证在 @BeforeAll@BeforeEach@AfterEach@AfterAll 失败后立即执行。

在错误时记录应用程序状态的异常处理扩展
class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler {

    @Override
    public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during class setup");
        throw ex;
    }

    @Override
    public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during test setup");
        throw ex;
    }

    @Override
    public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during test cleanup");
        throw ex;
    }

    @Override
    public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during class cleanup");
        throw ex;
    }
}

可以按声明顺序为同一个生命周期方法调用多个执行异常处理程序。如果其中一个处理程序吞并了处理的异常,则后续处理程序将不会执行,并且不会将任何错误传播到 JUnit 引擎,就好像从未抛出异常一样。处理程序也可以选择重新抛出异常或抛出不同的异常,可能包装原始异常。

实现 LifecycleMethodExecutionExceptionHandler 的扩展,希望处理在 @BeforeAll@AfterAll 期间抛出的异常,需要在类级别注册,而 BeforeEachAfterEach 的处理程序也可以为单个测试方法注册。

注册多个异常处理扩展
// Register handlers for @Test, @BeforeEach, @AfterEach as well as @BeforeAll and @AfterAll
@ExtendWith(ThirdExecutedHandler.class)
class MultipleHandlersTestCase {

    // Register handlers for @Test, @BeforeEach, @AfterEach only
    @ExtendWith(SecondExecutedHandler.class)
    @ExtendWith(FirstExecutedHandler.class)
    @Test
    void testMethod() {
    }

}

5.12. 拦截调用

InvocationInterceptor 定义了 Extensions 的 API,这些 Extensions 希望拦截对测试代码的调用。

以下示例展示了一个扩展,它在 Swing 的事件调度线程中执行所有测试方法。

在用户定义的线程中执行测试的扩展
public class SwingEdtInterceptor implements InvocationInterceptor {

    @Override
    public void interceptTestMethod(Invocation<Void> invocation,
            ReflectiveInvocationContext<Method> invocationContext,
            ExtensionContext extensionContext) throws Throwable {

        AtomicReference<Throwable> throwable = new AtomicReference<>();

        SwingUtilities.invokeAndWait(() -> {
            try {
                invocation.proceed();
            }
            catch (Throwable t) {
                throwable.set(t);
            }
        });
        Throwable t = throwable.get();
        if (t != null) {
            throw t;
        }
    }
}

5.13. 为测试模板提供调用上下文

一个 @TestTemplate 方法只有在注册了至少一个 TestTemplateInvocationContextProvider 时才能执行。每个这样的提供者负责提供一个 StreamTestTemplateInvocationContext 实例。每个上下文可以指定一个自定义的显示名称和一个额外的扩展列表,这些扩展只会在下一次调用 @TestTemplate 方法时使用。

以下示例展示了如何编写测试模板以及如何注册和实现 TestTemplateInvocationContextProvider

带有配套扩展的测试模板
final List<String> fruits = Arrays.asList("apple", "banana", "lemon");

@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String fruit) {
    assertTrue(fruits.contains(fruit));
}

public class MyTestTemplateInvocationContextProvider
        implements TestTemplateInvocationContextProvider {

    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return true;
    }

    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
            ExtensionContext context) {

        return Stream.of(invocationContext("apple"), invocationContext("banana"));
    }

    private TestTemplateInvocationContext invocationContext(String parameter) {
        return new TestTemplateInvocationContext() {
            @Override
            public String getDisplayName(int invocationIndex) {
                return parameter;
            }

            @Override
            public List<Extension> getAdditionalExtensions() {
                return Collections.singletonList(new ParameterResolver() {
                    @Override
                    public boolean supportsParameter(ParameterContext parameterContext,
                            ExtensionContext extensionContext) {
                        return parameterContext.getParameter().getType().equals(String.class);
                    }

                    @Override
                    public Object resolveParameter(ParameterContext parameterContext,
                            ExtensionContext extensionContext) {
                        return parameter;
                    }
                });
            }
        };
    }
}

在这个例子中,测试模板将被调用两次。调用的显示名称将是 applebanana,如调用上下文所指定。每次调用都会注册一个自定义的 ParameterResolver,它用于解析方法参数。使用 ConsoleLauncher 时,输出如下。

└─ testTemplate(String) ✔
   ├─ apple ✔
   └─ banana ✔

TestTemplateInvocationContextProvider 扩展 API 主要用于实现不同类型的测试,这些测试依赖于对类似测试的方法的重复调用,尽管是在不同的上下文中——例如,使用不同的参数,通过以不同的方式准备测试类实例,或者多次调用而不修改上下文。请参考 重复测试参数化测试 的实现,它们使用此扩展点来提供其功能。

5.14. 在扩展中保持状态

通常,扩展只实例化一次。所以问题就变成了:如何将扩展的一次调用中的状态保持到下一次调用?ExtensionContext API 提供了一个 Store,专门用于此目的。扩展可以将值放入存储中,以便以后检索。请参阅 TimingExtension,它展示了如何使用 Store 来实现方法级作用域。重要的是要记住,在测试执行期间存储在 ExtensionContext 中的值在周围的 ExtensionContext 中将不可用。由于 ExtensionContexts 可能嵌套,内部上下文的范围也可能受到限制。有关通过 Store 存储和检索值的可用方法的详细信息,请参阅相应的 Javadoc。

ExtensionContext.Store.CloseableResource
扩展上下文存储与其扩展上下文生命周期绑定。当扩展上下文生命周期结束时,它会关闭其关联的存储。所有存储的值,如果它们是 CloseableResource 的实例,都会通过调用其 close() 方法来通知它们,通知顺序与它们添加时的顺序相反。

5.15. 扩展中支持的实用程序

junit-platform-commons 工件公开了一个名为 org.junit.platform.commons.support 的包,其中包含用于处理注释、类、反射和类路径扫描任务的维护的实用程序方法。TestEngineExtension 作者鼓励使用这些支持的方法,以与 JUnit Platform 的行为保持一致。

5.15.1. 注释支持

AnnotationSupport 提供了对带注释元素(例如,包、注释、类、接口、构造函数、方法和字段)进行操作的静态实用程序方法。这些方法包括检查元素是否使用特定注释或元注释进行注释、搜索特定注释以及在类或接口中查找带注释的方法和字段。其中一些方法会在已实现的接口和类层次结构中搜索以查找注释。有关更多详细信息,请参阅 AnnotationSupport 的 Javadoc。

5.15.2. 类支持

ClassSupport 提供了用于处理类(即 java.lang.Class 的实例)的静态实用程序方法。有关更多详细信息,请参阅 ClassSupport 的 Javadoc。

5.15.3. 反射支持

ReflectionSupport 提供了增强标准 JDK 反射和类加载机制的静态实用程序方法。这些方法包括扫描类路径以搜索匹配指定谓词的类、加载和创建类的新的实例以及查找和调用方法。其中一些方法会遍历类层次结构以查找匹配的方法。有关更多详细信息,请参阅 ReflectionSupport 的 Javadoc。

5.15.4. 修饰符支持

ModifierSupport 提供了用于处理成员和类修饰符的静态实用程序方法——例如,确定成员是否声明为 publicprivateabstractstatic 等。有关更多详细信息,请参阅 ModifierSupport 的 Javadoc。

5.16. 用户代码和扩展的相对执行顺序

当执行包含一个或多个测试方法的测试类时,除了用户提供的测试和生命周期方法之外,还会调用许多扩展回调。

另请参阅:测试执行顺序

5.16.1. 用户代码和扩展代码

下图说明了用户提供的代码和扩展代码的相对顺序。用户提供的测试和生命周期方法以橙色显示,扩展实现的回调代码以蓝色显示。灰色框表示单个测试方法的执行,并且将针对测试类中的每个测试方法重复执行。

extensions lifecycle
用户代码和扩展代码

下表进一步解释了 用户代码和扩展代码 图中的十六个步骤。

步骤 接口/注释 描述

1

接口 org.junit.jupiter.api.extension.BeforeAllCallback

在执行容器的所有测试之前执行的扩展代码

2

注释 org.junit.jupiter.api.BeforeAll

在执行容器的所有测试之前执行的用户代码

3

接口 org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleBeforeAllMethodExecutionException

用于处理从 @BeforeAll 方法抛出的异常的扩展代码

4

接口 org.junit.jupiter.api.extension.BeforeEachCallback

在执行每个测试之前执行的扩展代码

5

注释 org.junit.jupiter.api.BeforeEach

在执行每个测试之前执行的用户代码

6

接口 org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleBeforeEachMethodExecutionException

用于处理从 @BeforeEach 方法抛出的异常的扩展代码

7

接口 org.junit.jupiter.api.extension.BeforeTestExecutionCallback

在执行测试之前立即执行的扩展代码

8

注释 org.junit.jupiter.api.Test

实际测试方法的用户代码

9

接口 org.junit.jupiter.api.extension.TestExecutionExceptionHandler

用于处理测试期间抛出的异常的扩展代码

10

接口 org.junit.jupiter.api.extension.AfterTestExecutionCallback

在测试执行及其相应的异常处理程序之后立即执行的扩展代码

11

注释 org.junit.jupiter.api.AfterEach

在执行每个测试之后执行的用户代码

12

接口 org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleAfterEachMethodExecutionException

用于处理从 @AfterEach 方法抛出的异常的扩展代码

13

接口 org.junit.jupiter.api.extension.AfterEachCallback

在执行每个测试之后执行的扩展代码

14

注释 org.junit.jupiter.api.AfterAll

在执行容器的所有测试之后执行的用户代码

15

接口 org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleAfterAllMethodExecutionException

用于处理从 @AfterAll 方法抛出的异常的扩展代码

16

接口 org.junit.jupiter.api.extension.AfterAllCallback

在执行容器的所有测试之后执行的扩展代码

在最简单的情况下,只会执行实际的测试方法(步骤 8);所有其他步骤都是可选的,具体取决于是否存在用户代码或扩展对相应生命周期回调的支持。有关各种生命周期回调的更多详细信息,请参阅每个注释和扩展的相应 Javadoc。

上表中所有用户代码方法的调用都可以通过实现 InvocationInterceptor 来拦截。

5.16.2. 回调的包装行为

JUnit Jupiter 始终保证对注册的多个实现生命周期回调的扩展(例如 BeforeAllCallbackAfterAllCallbackBeforeEachCallbackAfterEachCallbackBeforeTestExecutionCallbackAfterTestExecutionCallback)的包装行为。

这意味着,给定两个扩展 Extension1Extension2,其中 Extension1Extension2 之前注册,则保证 Extension1 实现的任何“before”回调都将在 Extension2 实现的任何“before”回调之前执行。类似地,给定这两个相同扩展,以相同的顺序注册,则保证 Extension1 实现的任何“after”回调都将在 Extension2 实现的任何“after”回调之后执行。因此,Extension1 被称为包装 Extension2

JUnit Jupiter 还保证用户提供的生命周期方法(请参阅 定义)在类和接口层次结构中的包装行为。

  • @BeforeAll 方法从超类继承,只要它们没有被隐藏覆盖取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的 @BeforeAll 方法将在子类中的 @BeforeAll 方法之前执行。

    • 类似地,在接口中声明的 @BeforeAll 方法将被继承,只要它们没有被隐藏覆盖,并且接口中的 @BeforeAll 方法将在实现该接口的类中的 @BeforeAll 方法之前执行。

  • @AfterAll 方法从超类继承,只要它们没有被隐藏覆盖取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的 @AfterAll 方法将在子类中的 @AfterAll 方法之后执行。

    • 类似地,在接口中声明的 @AfterAll 方法将被继承,只要它们没有被隐藏覆盖,并且接口中的 @AfterAll 方法将在实现该接口的类中的 @AfterAll 方法之后执行。

  • @BeforeEach 方法从超类继承,只要它们没有被覆盖取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的 @BeforeEach 方法将在子类中的 @BeforeEach 方法之前执行。

    • 类似地,声明为接口默认方法的 @BeforeEach 方法将被继承,只要它们没有被覆盖,并且默认方法将在实现该接口的类中的 @BeforeEach 方法之前执行。

  • @AfterEach 方法从超类继承,只要它们没有被覆盖取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的 @AfterEach 方法将在子类中的 @AfterEach 方法之后执行。

    • 类似地,声明为接口默认方法的 @AfterEach 方法将被继承,只要它们没有被覆盖,并且默认方法将在实现该接口的类中的 @AfterEach 方法之后执行。

以下示例演示了这种行为。请注意,这些示例实际上并没有做任何实际的事情。相反,它们模拟了用于测试与数据库交互的常见场景。从Logger类静态导入的所有方法都记录上下文信息,以便帮助我们更好地理解用户提供的回调方法和扩展中的回调方法的执行顺序。

扩展1
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;

import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

public class Extension1 implements BeforeEachCallback, AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        beforeEachCallback(this);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        afterEachCallback(this);
    }

}
扩展2
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;

import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

public class Extension2 implements BeforeEachCallback, AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        beforeEachCallback(this);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        afterEachCallback(this);
    }

}
抽象数据库测试
import static example.callbacks.Logger.afterAllMethod;
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;

/**
 * Abstract base class for tests that use the database.
 */
abstract class AbstractDatabaseTests {

    @BeforeAll
    static void createDatabase() {
        beforeAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".createDatabase()");
    }

    @BeforeEach
    void connectToDatabase() {
        beforeEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".connectToDatabase()");
    }

    @AfterEach
    void disconnectFromDatabase() {
        afterEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".disconnectFromDatabase()");
    }

    @AfterAll
    static void destroyDatabase() {
        afterAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".destroyDatabase()");
    }

}
数据库测试演示
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/**
 * Extension of {@link AbstractDatabaseTests} that inserts test data
 * into the database (after the database connection has been opened)
 * and deletes test data (before the database connection is closed).
 */
@ExtendWith({ Extension1.class, Extension2.class })
class DatabaseTestsDemo extends AbstractDatabaseTests {

    @BeforeAll
    static void beforeAll() {
        beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".beforeAll()");
    }

    @BeforeEach
    void insertTestDataIntoDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
    }

    @Test
    void testDatabaseFunctionality() {
        testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
    }

    @AfterEach
    void deleteTestDataFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
    }

    @AfterAll
    static void afterAll() {
        beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".afterAll()");
    }

}

当执行DatabaseTestsDemo测试类时,将记录以下内容。

@BeforeAll AbstractDatabaseTests.createDatabase()
@BeforeAll DatabaseTestsDemo.beforeAll()
  Extension1.beforeEach()
  Extension2.beforeEach()
    @BeforeEach AbstractDatabaseTests.connectToDatabase()
    @BeforeEach DatabaseTestsDemo.insertTestDataIntoDatabase()
      @Test DatabaseTestsDemo.testDatabaseFunctionality()
    @AfterEach DatabaseTestsDemo.deleteTestDataFromDatabase()
    @AfterEach AbstractDatabaseTests.disconnectFromDatabase()
  Extension2.afterEach()
  Extension1.afterEach()
@BeforeAll DatabaseTestsDemo.afterAll()
@AfterAll AbstractDatabaseTests.destroyDatabase()

以下时序图有助于进一步阐明当执行DatabaseTestsDemo测试类时,JupiterTestEngine内部实际发生了什么。

extensions DatabaseTestsDemo
数据库测试演示

JUnit Jupiter **不**保证在单个测试类或测试接口中声明的多个生命周期方法的执行顺序。有时可能看起来 JUnit Jupiter 按字母顺序调用这些方法。但是,这并不完全正确。排序类似于单个测试类中@Test方法的排序。

单个测试类或测试接口中声明的生命周期方法将使用确定性但有意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行生命周期方法,从而允许可重复的构建。

此外,JUnit Jupiter **不支持**在单个测试类或测试接口中声明的多个生命周期方法的包装行为。

以下示例演示了这种行为。具体来说,生命周期方法配置由于本地声明的生命周期方法的执行顺序而损坏

  • 测试数据在打开数据库连接之前插入,这会导致无法连接到数据库。

  • 数据库连接在删除测试数据之前关闭,这会导致无法连接到数据库。

损坏的生命周期方法配置演示
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/**
 * Example of "broken" lifecycle method configuration.
 *
 * <p>Test data is inserted before the database connection has been opened.
 *
 * <p>Database connection is closed before deleting test data.
 */
@ExtendWith({ Extension1.class, Extension2.class })
class BrokenLifecycleMethodConfigDemo {

    @BeforeEach
    void connectToDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".connectToDatabase()");
    }

    @BeforeEach
    void insertTestDataIntoDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
    }

    @Test
    void testDatabaseFunctionality() {
        testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
    }

    @AfterEach
    void deleteTestDataFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
    }

    @AfterEach
    void disconnectFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".disconnectFromDatabase()");
    }

}

当执行BrokenLifecycleMethodConfigDemo测试类时,将记录以下内容。

Extension1.beforeEach()
Extension2.beforeEach()
  @BeforeEach BrokenLifecycleMethodConfigDemo.insertTestDataIntoDatabase()
  @BeforeEach BrokenLifecycleMethodConfigDemo.connectToDatabase()
    @Test BrokenLifecycleMethodConfigDemo.testDatabaseFunctionality()
  @AfterEach BrokenLifecycleMethodConfigDemo.disconnectFromDatabase()
  @AfterEach BrokenLifecycleMethodConfigDemo.deleteTestDataFromDatabase()
Extension2.afterEach()
Extension1.afterEach()

以下时序图有助于进一步阐明当执行BrokenLifecycleMethodConfigDemo测试类时,JupiterTestEngine内部实际发生了什么。

extensions BrokenLifecycleMethodConfigDemo
损坏的生命周期方法配置演示

由于上述行为,JUnit 团队建议开发人员在每个测试类或测试接口中最多声明一种类型的生命周期方法(参见定义),除非这些生命周期方法之间没有依赖关系。

6. 高级主题

6.1. JUnit 平台报告

junit-platform-reporting 工件包含TestExecutionListener 实现,这些实现以两种形式生成 XML 测试报告:传统Open Test Reporting

该模块还包含其他TestExecutionListener 实现,可用于构建自定义报告。有关详细信息,请参见使用监听器和拦截器

6.1.1. 传统 XML 格式

LegacyXmlReportGeneratingListenerTestPlan 中的每个根生成一个单独的 XML 报告。请注意,生成的 XML 格式与基于 JUnit 4 的测试报告的事实标准兼容,该标准因 Ant 构建系统而流行。

LegacyXmlReportGeneratingListener 也被控制台启动器 使用。

6.1.2. Open Test Reporting XML 格式

OpenTestReportGeneratingListenerOpen Test Reporting 指定的基于事件的格式为整个执行写入 XML 报告,该格式支持 JUnit 平台的所有功能,例如分层测试结构、显示名称、标签等。

该监听器会自动注册,并且可以通过以下配置参数 进行配置

junit.platform.reporting.open.xml.enabled=true|false

启用/禁用写入报告。

junit.platform.reporting.output.dir=<path>

配置报告的输出目录。默认情况下,如果找到 Gradle 构建脚本,则使用build,如果找到 Maven POM,则使用target;否则,使用当前工作目录。

如果启用,该监听器将在配置的输出目录中为每次测试运行创建一个名为junit-platform-events-<random-id>.xml 的 XML 报告文件。

可以使用Open Test Reporting CLI 工具 从基于事件的格式转换为分层格式,该格式更易于人类阅读。
Gradle

对于 Gradle,可以通过系统属性启用和配置写入与 Open Test Reporting 兼容的 XML 报告。以下示例将其输出目录配置为与 Gradle 用于其自身 XML 报告的目录相同。CommandLineArgumentProvider 用于使任务在不同机器之间可重定位,这在使用 Gradle 的构建缓存时很重要。

Groovy DSL
dependencies {
    testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.2")
}
tasks.withType(Test).configureEach {
    def outputDir = reports.junitXml.outputLocation
    jvmArgumentProviders << ({
        [
            "-Djunit.platform.reporting.open.xml.enabled=true",
            "-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}"
        ]
    } as CommandLineArgumentProvider)
}
Kotlin DSL
dependencies {
    testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.2")
}
tasks.withType<Test>().configureEach {
    val outputDir = reports.junitXml.outputLocation
    jvmArgumentProviders += CommandLineArgumentProvider {
        listOf(
            "-Djunit.platform.reporting.open.xml.enabled=true",
            "-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}"
        )
    }
}
Maven

对于 Maven Surefire/Failsafe,您可以启用 Open Test Reporting 输出,并将生成的 XML 文件配置为写入 Surefire/Failsafe 用于其自身 XML 报告的相同目录,如下所示

<project>
    <!-- ... -->
    <dependencies>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-reporting</artifactId>
            <version>1.10.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
                <configuration>
                    <properties>
                        <configurationParameters>
                            junit.platform.reporting.open.xml.enabled = true
                            junit.platform.reporting.output.dir = target/surefire-reports
                        </configurationParameters>
                    </properties>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <!-- ... -->
</project>
控制台启动器

使用控制台启动器 时,可以通过--config 设置配置参数来启用 Open Test Reporting 输出

$ java -jar junit-platform-console-standalone-1.10.2.jar <OPTIONS> \
  --config=junit.platform.reporting.open.xml.enabled=true \
  --config=junit.platform.reporting.output.dir=reports

6.2. JUnit 平台套件引擎

JUnit 平台支持使用 JUnit 平台从任何测试引擎声明式定义和执行测试套件。

6.2.1. 设置

除了junit-platform-suite-apijunit-platform-suite-engine 工件之外,您还需要类路径上的至少一个其他测试引擎及其依赖项。有关组 ID、工件 ID 和版本的详细信息,请参见依赖项元数据

必需的依赖项
  • junit-platform-suite-api测试范围内:包含用于配置测试套件的注释的工件

  • junit-platform-suite-engine测试运行时范围内:用于声明式测试套件的TestEngine API 的实现

这两个必需的依赖项都聚合在junit-platform-suite 工件中,该工件可以在测试范围内声明,而不是显式声明对junit-platform-suite-apijunit-platform-suite-engine 的依赖项。
传递依赖项
  • 测试范围内的 `junit-platform-suite-commons`

  • 测试范围内的 `junit-platform-launcher`

  • 测试范围内的 `junit-platform-engine`

  • 测试范围内的 `junit-platform-commons`

  • 测试范围内的 `opentest4j`

6.2.2. @Suite 示例

通过使用@Suite 注释类,它被标记为 JUnit 平台上的测试套件。如以下示例所示,然后可以使用选择器和过滤器注释来控制套件的内容。

import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;

@Suite
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
@IncludeClassNamePatterns(".*Tests")
class SuiteDemo {
}
其他配置选项
在测试套件中发现和过滤测试有许多配置选项。有关支持的注释的完整列表和更多详细信息,请参阅org.junit.platform.suite.api 包的 Javadoc。

6.3. JUnit 平台测试工具包

junit-platform-testkit 工件提供了在 JUnit 平台上执行测试计划并验证预期结果的支持。从 JUnit 平台 1.4 开始,此支持仅限于执行单个TestEngine(参见引擎测试工具包)。

6.3.1. 引擎测试工具包

org.junit.platform.testkit.engine 包提供了在 JUnit 平台上执行给定TestPlan 的支持,然后通过流畅的 API 访问结果以验证预期结果。此 API 的关键入口点是EngineTestKit,它提供名为engine()execute() 的静态工厂方法。建议您选择其中一个engine() 变体以从用于构建LauncherDiscoveryRequest 的流畅 API 中获益。

如果您希望使用Launcher API 中的LauncherDiscoveryRequestBuilder 来构建您的LauncherDiscoveryRequest,则必须在EngineTestKit 中使用其中一个execute() 变体。

以下使用 JUnit Jupiter 编写的测试类将在后续示例中使用。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import example.util.Calculator;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(OrderAnnotation.class)
public class ExampleTestCase {

    private final Calculator calculator = new Calculator();

    @Test
    @Disabled("for demonstration purposes")
    @Order(1)
    void skippedTest() {
        // skipped ...
    }

    @Test
    @Order(2)
    void succeedingTest() {
        assertEquals(42, calculator.multiply(6, 7));
    }

    @Test
    @Order(3)
    void abortedTest() {
        assumeTrue("abc".contains("Z"), "abc does not contain Z");
        // aborted ...
    }

    @Test
    @Order(4)
    void failingTest() {
        // The following throws an ArithmeticException: "/ by zero"
        calculator.divide(1, 0);
    }

}

为了简洁起见,以下部分演示了如何测试 JUnit 自身的JupiterTestEngine,其唯一的引擎 ID 为"junit-jupiter"。如果您想测试自己的TestEngine 实现,则需要使用其唯一的引擎 ID。或者,您可以通过向EngineTestKit.engine(TestEngine) 静态工厂方法提供其实例来测试自己的TestEngine

6.3.2. 断言统计信息

测试工具包最常见的特性之一是能够对在执行TestPlan 期间触发的事件断言统计信息。以下测试演示了如何对 JUnit JupiterTestEngine 中的容器测试断言统计信息。有关可用统计信息的详细信息,请参阅EventStatistics 的 Javadoc。

import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;

import example.ExampleTestCase;

import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;

class EngineTestKitStatisticsDemo {

    @Test
    void verifyJupiterContainerStats() {
        EngineTestKit
            .engine("junit-jupiter") (1)
            .selectors(selectClass(ExampleTestCase.class)) (2)
            .execute() (3)
            .containerEvents() (4)
            .assertStatistics(stats -> stats.started(2).succeeded(2)); (5)
    }

    @Test
    void verifyJupiterTestStats() {
        EngineTestKit
            .engine("junit-jupiter") (1)
            .selectors(selectClass(ExampleTestCase.class)) (2)
            .execute() (3)
            .testEvents() (6)
            .assertStatistics(stats ->
                stats.skipped(1).started(3).succeeded(1).aborted(1).failed(1)); (7)
    }

}
1 选择 JUnit JupiterTestEngine
2 选择ExampleTestCase 测试类。
3 执行TestPlan
4 容器事件过滤。
5 断言容器事件的统计信息。
6 测试事件过滤。
7 断言测试事件的统计信息。
verifyJupiterContainerStats() 测试方法中,startedsucceeded 统计信息的计数为2,因为JupiterTestEngineExampleTestCase 类都被视为容器。

6.3.3. 断言事件

如果您发现断言统计信息 对于验证测试执行的预期行为还不够,您可以直接使用记录的Event 元素并对其执行断言。

例如,如果您想验证ExampleTestCase 中的skippedTest() 方法被跳过的原因,您可以按如下方式执行。

以下示例中的assertThatEvents() 方法是org.assertj.core.api.Assertions.assertThat(events.list()) 的快捷方式,来自AssertJ 断言库。

有关可用于对事件进行 AssertJ 断言的条件的详细信息,请参阅EventConditions 的 Javadoc。

import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.test;

import example.ExampleTestCase;

import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.platform.testkit.engine.Events;

class EngineTestKitSkippedMethodDemo {

    @Test
    void verifyJupiterMethodWasSkipped() {
        String methodName = "skippedTest";

        Events testEvents = EngineTestKit (5)
            .engine("junit-jupiter") (1)
            .selectors(selectMethod(ExampleTestCase.class, methodName)) (2)
            .execute() (3)
            .testEvents(); (4)

        testEvents.assertStatistics(stats -> stats.skipped(1)); (6)

        testEvents.assertThatEvents() (7)
            .haveExactly(1, event(test(methodName),
                skippedWithReason("for demonstration purposes")));
    }

}
1 选择 JUnit JupiterTestEngine
2 选择ExampleTestCase 测试类中的skippedTest() 方法。
3 执行TestPlan
4 测试事件过滤。
5 测试Events 保存到本地变量中。
6 可选地断言预期统计信息。
7 断言记录的测试事件包含一个名为skippedTest 的跳过测试,其原因"for demonstration purposes"

如果您想验证从ExampleTestCase 中的failingTest() 方法抛出的异常类型,您可以按如下方式执行。

有关可用于针对事件和执行结果的 AssertJ 断言的条件的详细信息,请分别查阅 EventConditionsTestExecutionResultConditions 的 Javadoc。

import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;

import example.ExampleTestCase;

import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;

class EngineTestKitFailedMethodDemo {

    @Test
    void verifyJupiterMethodFailed() {
        EngineTestKit.engine("junit-jupiter") (1)
            .selectors(selectClass(ExampleTestCase.class)) (2)
            .execute() (3)
            .testEvents() (4)
            .assertThatEvents().haveExactly(1, (5)
                event(test("failingTest"),
                    finishedWithFailure(
                        instanceOf(ArithmeticException.class), message("/ by zero"))));
    }

}
1 选择 JUnit JupiterTestEngine
2 选择ExampleTestCase 测试类。
3 执行TestPlan
4 测试事件过滤。
5 断言记录的测试事件包含一个名为 failingTest 的失败测试,该测试的异常类型为 ArithmeticException,错误消息为 "/ by zero"

虽然通常不需要,但有时您需要验证在执行 TestPlan 期间触发的所有事件。以下测试演示了如何通过 EngineTestKit API 中的 assertEventsMatchExactly() 方法来实现这一点。

由于 assertEventsMatchExactly() 严格按照事件触发的顺序匹配条件,因此 ExampleTestCase 已使用 @TestMethodOrder(OrderAnnotation.class) 进行注释,每个测试方法都已使用 @Order(…​) 进行注释。这使我们能够强制执行测试方法的执行顺序,从而使我们的 verifyAllJupiterEvents() 测试变得可靠。

如果您想进行部分匹配,没有排序要求,您可以分别使用 assertEventsMatchLooselyInOrder()assertEventsMatchLoosely() 方法。

import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.container;
import static org.junit.platform.testkit.engine.EventConditions.engine;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.started;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;

import java.io.StringWriter;
import java.io.Writer;

import example.ExampleTestCase;

import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.opentest4j.TestAbortedException;

class EngineTestKitAllEventsDemo {

    @Test
    void verifyAllJupiterEvents() {
        Writer writer = // create a java.io.Writer for debug output

        EngineTestKit.engine("junit-jupiter") (1)
            .selectors(selectClass(ExampleTestCase.class)) (2)
            .execute() (3)
            .allEvents() (4)
            .debug(writer) (5)
            .assertEventsMatchExactly( (6)
                event(engine(), started()),
                event(container(ExampleTestCase.class), started()),
                event(test("skippedTest"), skippedWithReason("for demonstration purposes")),
                event(test("succeedingTest"), started()),
                event(test("succeedingTest"), finishedSuccessfully()),
                event(test("abortedTest"), started()),
                event(test("abortedTest"),
                    abortedWithReason(instanceOf(TestAbortedException.class),
                        message(m -> m.contains("abc does not contain Z")))),
                event(test("failingTest"), started()),
                event(test("failingTest"), finishedWithFailure(
                    instanceOf(ArithmeticException.class), message("/ by zero"))),
                event(container(ExampleTestCase.class), finishedSuccessfully()),
                event(engine(), finishedSuccessfully()));
    }

}
1 选择 JUnit JupiterTestEngine
2 选择ExampleTestCase 测试类。
3 执行TestPlan
4 所有事件过滤。
5 将所有事件打印到提供的 writer 中,以供调试。调试信息也可以写入 OutputStream,例如 System.outSystem.err
6 断言所有事件,严格按照测试引擎触发的顺序。

前面的示例中的 debug() 调用产生的输出类似于以下内容。

All Events:
    Event [type = STARTED, testDescriptor = JupiterEngineDescriptor: [engine:junit-jupiter], timestamp = 2018-12-14T12:45:14.082280Z, payload = null]
    Event [type = STARTED, testDescriptor = ClassTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase], timestamp = 2018-12-14T12:45:14.089339Z, payload = null]
    Event [type = SKIPPED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:skippedTest()], timestamp = 2018-12-14T12:45:14.094314Z, payload = 'for demonstration purposes']
    Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:succeedingTest()], timestamp = 2018-12-14T12:45:14.095182Z, payload = null]
    Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:succeedingTest()], timestamp = 2018-12-14T12:45:14.104922Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
    Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:abortedTest()], timestamp = 2018-12-14T12:45:14.106121Z, payload = null]
    Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:abortedTest()], timestamp = 2018-12-14T12:45:14.109956Z, payload = TestExecutionResult [status = ABORTED, throwable = org.opentest4j.TestAbortedException: Assumption failed: abc does not contain Z]]
    Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:failingTest()], timestamp = 2018-12-14T12:45:14.110680Z, payload = null]
    Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:failingTest()], timestamp = 2018-12-14T12:45:14.111217Z, payload = TestExecutionResult [status = FAILED, throwable = java.lang.ArithmeticException: / by zero]]
    Event [type = FINISHED, testDescriptor = ClassTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase], timestamp = 2018-12-14T12:45:14.113731Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
    Event [type = FINISHED, testDescriptor = JupiterEngineDescriptor: [engine:junit-jupiter], timestamp = 2018-12-14T12:45:14.113806Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]

6.4. JUnit Platform Launcher API

JUnit 5 的主要目标之一是使 JUnit 与其编程客户端(构建工具和 IDE)之间的接口更加强大和稳定。目的是将发现和执行测试的内部机制与从外部进行的所有过滤和配置分离。

JUnit 5 引入了 Launcher 的概念,可用于发现、过滤和执行测试。此外,第三方测试库(如 Spock、Cucumber 和 FitNesse)可以通过提供自定义的 TestEngine 来插入 JUnit Platform 的启动基础设施。

启动器 API 位于 junit-platform-launcher 模块中。

启动器 API 的一个示例使用者是 ConsoleLauncher,它位于 junit-platform-console 项目中。

6.4.1. 发现测试

测试发现作为平台本身的专用功能,使 IDE 和构建工具免于在 JUnit 的先前版本中识别测试类和测试方法所遇到的许多困难。

用法示例

import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;

import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.junit.platform.engine.FilterResult;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherConfig;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import org.junit.platform.reporting.legacy.xml.LegacyXmlReportGeneratingListener;
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
    .selectors(
        selectPackage("com.example.mytests"),
        selectClass(MyTestClass.class)
    )
    .filters(
        includeClassNamePatterns(".*Tests")
    )
    .build();

try (LauncherSession session = LauncherFactory.openSession()) {
    TestPlan testPlan = session.getLauncher().discover(request);

    // ... discover additional test plans or execute tests
}

您可以选择类、方法和包中的所有类,甚至搜索类路径或模块路径中的所有测试。发现发生在所有参与的测试引擎中。

生成的 TestPlan 是所有符合 LauncherDiscoveryRequest 的引擎、类和测试方法的分层(只读)描述。客户端可以遍历树,检索有关节点的详细信息,并获取指向原始源(如类、方法或文件位置)的链接。测试计划中的每个节点都有一个唯一 ID,可用于调用特定测试或测试组。

客户端可以通过 LauncherDiscoveryRequestBuilder 注册一个或多个 LauncherDiscoveryListener 实现,以深入了解测试发现期间发生的事件。默认情况下,构建器注册一个“在失败时中止”监听器,该监听器在遇到第一个发现失败后中止测试发现。默认的 LauncherDiscoveryListener 可以通过 junit.platform.discovery.listener.default 配置参数 进行更改。

6.4.2. 执行测试

要执行测试,客户端可以使用与发现阶段相同的 LauncherDiscoveryRequest,也可以创建新的请求。可以通过向 Launcher 注册一个或多个 TestExecutionListener 实现来实现测试进度和报告,如下例所示。

LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
    .selectors(
        selectPackage("com.example.mytests"),
        selectClass(MyTestClass.class)
    )
    .filters(
        includeClassNamePatterns(".*Tests")
    )
    .build();

SummaryGeneratingListener listener = new SummaryGeneratingListener();

try (LauncherSession session = LauncherFactory.openSession()) {
    Launcher launcher = session.getLauncher();
    // Register a listener of your choice
    launcher.registerTestExecutionListeners(listener);
    // Discover tests and build a test plan
    TestPlan testPlan = launcher.discover(request);
    // Execute test plan
    launcher.execute(testPlan);
    // Alternatively, execute the request directly
    launcher.execute(request);
}

TestExecutionSummary summary = listener.getSummary();
// Do something with the summary...

execute() 方法没有返回值,但您可以使用 TestExecutionListener 来聚合结果。例如,请参阅 SummaryGeneratingListenerLegacyXmlReportGeneratingListenerUniqueIdTrackingListener

所有 TestExecutionListener 方法都按顺序调用。开始事件的方法按注册顺序调用,而结束事件的方法按相反顺序调用。测试用例执行在所有 executionStarted 调用返回之前不会开始。

6.4.3. 注册 TestEngine

有关详细信息,请参阅有关 TestEngine 注册 的专用部分。

6.4.4. 注册 PostDiscoveryFilter

除了在传递给 Launcher API 的 LauncherDiscoveryRequest 中指定后发现过滤器之外,PostDiscoveryFilter 实现将在运行时通过 Java 的 ServiceLoader 机制被发现,并由 Launcher 自动应用,除了那些作为请求的一部分的过滤器之外。

例如,在 /META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter 文件中声明的实现 PostDiscoveryFilterexample.CustomTagFilter 类将被加载并自动应用。

6.4.5. 注册 LauncherSessionListener

注册的 LauncherSessionListener 实现将在打开 LauncherSession(在 Launcher 首次发现和执行测试之前)和关闭(当不再发现或执行测试时)时收到通知。它们可以通过传递给 LauncherFactoryLauncherConfig 以编程方式注册,或者它们可以通过 Java 的 ServiceLoader 机制在运行时被发现,并自动注册到 LauncherSession(除非禁用自动注册)。

工具支持

以下构建工具和 IDE 据悉为 LauncherSession 提供了完全支持

  • Gradle 4.6 及更高版本

  • Maven Surefire/Failsafe 3.0.0-M6 及更高版本

  • IntelliJ IDEA 2017.3 及更高版本

其他工具也可能有效,但尚未经过明确测试。

用法示例

LauncherSessionListener 非常适合实现一次性 JVM 设置/拆卸行为,因为它分别在启动器会话中的第一个测试之前和最后一个测试之后调用。启动器会话的范围取决于使用的 IDE 或构建工具,但通常对应于测试 JVM 的生命周期。一个在执行第一个测试之前启动 HTTP 服务器并在最后一个测试执行完毕后停止它的自定义监听器可能如下所示

src/test/java/example/session/GlobalSetupTeardownListener.java
package example.session;

import static java.net.InetAddress.getLoopbackAddress;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.sun.net.httpserver.HttpServer;

import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;

public class GlobalSetupTeardownListener implements LauncherSessionListener {

    private Fixture fixture;

    @Override
    public void launcherSessionOpened(LauncherSession session) {
        // Avoid setup for test discovery by delaying it until tests are about to be executed
        session.getLauncher().registerTestExecutionListeners(new TestExecutionListener() {
            @Override
            public void testPlanExecutionStarted(TestPlan testPlan) {
                if (fixture == null) {
                    fixture = new Fixture();
                    fixture.setUp();
                }
            }
        });
    }

    @Override
    public void launcherSessionClosed(LauncherSession session) {
        if (fixture != null) {
            fixture.tearDown();
            fixture = null;
        }
    }

    static class Fixture {

        private HttpServer server;
        private ExecutorService executorService;

        void setUp() {
            try {
                server = HttpServer.create(new InetSocketAddress(getLoopbackAddress(), 0), 0);
            }
            catch (IOException e) {
                throw new UncheckedIOException("Failed to start HTTP server", e);
            }
            server.createContext("/test", exchange -> {
                exchange.sendResponseHeaders(204, -1);
                exchange.close();
            });
            executorService = Executors.newCachedThreadPool();
            server.setExecutor(executorService);
            server.start(); (1)
            int port = server.getAddress().getPort();
            System.setProperty("http.server.host", getLoopbackAddress().getHostAddress()); (2)
            System.setProperty("http.server.port", String.valueOf(port)); (3)
        }

        void tearDown() {
            server.stop(0); (4)
            executorService.shutdownNow();
        }
    }

}
1 启动 HTTP 服务器
2 将其主机地址导出为系统属性,供测试使用
3 将其端口导出为系统属性,供测试使用
4 停止 HTTP 服务器

此示例使用来自 JDK 的 jdk.httpserver 模块的 HTTP 服务器实现,但与任何其他服务器或资源的工作方式类似。为了使监听器被 JUnit Platform 拾取,您需要将其注册为服务,方法是在您的测试运行时类路径中添加一个具有以下名称和内容的资源文件(例如,通过将文件添加到 src/test/resources 中)

src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener
example.session.GlobalSetupTeardownListener

您现在可以使用测试中的资源

src/test/java/example/session/HttpTests.java
package example.session;

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

import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;

import org.junit.jupiter.api.Test;

class HttpTests {

    @Test
    void respondsWith204() throws Exception {
        String host = System.getProperty("http.server.host"); (1)
        String port = System.getProperty("http.server.port"); (2)
        URL url = URI.create("http://" + host + ":" + port + "/test").toURL();

        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        int responseCode = connection.getResponseCode(); (3)

        assertEquals(204, responseCode); (4)
    }
}
1 从监听器设置的系统属性中读取服务器的主机地址
2 从监听器设置的系统属性中读取服务器的端口
3 向服务器发送请求
4 检查响应的状态代码

6.4.6. 注册 LauncherInterceptor

为了拦截 LauncherLauncherSessionListener 实例的创建以及对前者的 discoverexecute 方法的调用,客户端可以通过 Java 的 ServiceLoader 机制注册 LauncherInterceptor 的自定义实现,方法是将 junit.platform.launcher.interceptors.enabled 配置参数 设置为 true

一个典型的用例是创建一个自定义的替换 JUnit Platform 用于加载测试类和引擎实现的 ClassLoader

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;

import org.junit.platform.launcher.LauncherInterceptor;

public class CustomLauncherInterceptor implements LauncherInterceptor {

    private final URLClassLoader customClassLoader;

    public CustomLauncherInterceptor() throws Exception {
        ClassLoader parent = Thread.currentThread().getContextClassLoader();
        customClassLoader = new URLClassLoader(new URL[] { URI.create("some.jar").toURL() }, parent);
    }

    @Override
    public <T> T intercept(Invocation<T> invocation) {
        Thread currentThread = Thread.currentThread();
        ClassLoader originalClassLoader = currentThread.getContextClassLoader();
        currentThread.setContextClassLoader(customClassLoader);
        try {
            return invocation.proceed();
        }
        finally {
            currentThread.setContextClassLoader(originalClassLoader);
        }
    }

    @Override
    public void close() {
        try {
            customClassLoader.close();
        }
        catch (IOException e) {
            throw new UncheckedIOException("Failed to close custom class loader", e);
        }
    }
}

6.4.7. 注册 LauncherDiscoveryListener

除了在 LauncherDiscoveryRequest 中指定发现监听器或通过 Launcher API 以编程方式注册它们之外,自定义的 LauncherDiscoveryListener 实现可以通过 Java 的 ServiceLoader 机制在运行时被发现,并自动注册到通过 LauncherFactory 创建的 Launcher

例如,在 /META-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener 文件中声明的实现 LauncherDiscoveryListenerexample.CustomLauncherDiscoveryListener 类将被加载并自动注册。

6.4.8. 注册 TestExecutionListener

除了用于以编程方式注册测试执行监听器的公共 Launcher API 方法之外,自定义的 TestExecutionListener 实现将在运行时通过 Java 的 ServiceLoader 机制被发现,并自动注册到通过 LauncherFactory 创建的 Launcher

例如,实现 TestExecutionListener 并声明在 /META-INF/services/org.junit.platform.launcher.TestExecutionListener 文件中的 example.CustomTestExecutionListener 类将自动加载和注册。

6.4.9. 配置 TestExecutionListener

TestExecutionListener 通过 Launcher API 以编程方式注册时,监听器可能提供以编程方式配置它的方法,例如,通过它的构造函数、setter 方法等。但是,当 TestExecutionListener 通过 Java 的 ServiceLoader 机制自动注册时(参见 注册 TestExecutionListener),用户无法直接配置监听器。在这种情况下,TestExecutionListener 的作者可以选择通过 配置参数 使监听器可配置。然后,监听器可以通过提供给 testPlanExecutionStarted(TestPlan)testPlanExecutionFinished(TestPlan) 回调方法的 TestPlan 访问配置参数。参见 UniqueIdTrackingListener 以获取示例。

6.4.10. 禁用 TestExecutionListener

有时,在某些执行监听器处于活动状态的情况下运行测试套件可能很有用。例如,您可能有一个自定义的 TestExecutionListener,它将测试结果发送到外部系统以进行报告,而在调试时,您可能不希望报告这些调试结果。为此,请为 junit.platform.execution.listeners.deactivate 配置参数提供一个模式,以指定应为当前测试运行禁用(即未注册)哪些执行监听器。

只有通过 /META-INF/services/org.junit.platform.launcher.TestExecutionListener 文件中的 ServiceLoader 机制注册的监听器才能被禁用。换句话说,任何通过 LauncherDiscoveryRequest 显式注册的 TestExecutionListener 无法通过 junit.platform.execution.listeners.deactivate 配置参数禁用。

此外,由于执行监听器在测试运行开始之前注册,因此 junit.platform.execution.listeners.deactivate 配置参数只能作为 JVM 系统属性提供,或者通过 JUnit Platform 配置文件提供(有关详细信息,请参见 配置参数)。此配置参数不能在传递给 LauncherLauncherDiscoveryRequest 中提供。

模式匹配语法

有关详细信息,请参阅模式匹配语法

6.4.11. 配置 Launcher

如果您需要对测试引擎和监听器的自动检测和注册进行细粒度控制,您可以创建 LauncherConfig 的实例,并将其提供给 LauncherFactory。通常,LauncherConfig 的实例是通过内置的流畅构建器 API 创建的,如下面的示例所示。

LauncherConfig launcherConfig = LauncherConfig.builder()
    .enableTestEngineAutoRegistration(false)
    .enableLauncherSessionListenerAutoRegistration(false)
    .enableLauncherDiscoveryListenerAutoRegistration(false)
    .enablePostDiscoveryFilterAutoRegistration(false)
    .enableTestExecutionListenerAutoRegistration(false)
    .addTestEngines(new CustomTestEngine())
    .addLauncherSessionListeners(new CustomLauncherSessionListener())
    .addLauncherDiscoveryListeners(new CustomLauncherDiscoveryListener())
    .addPostDiscoveryFilters(new CustomPostDiscoveryFilter())
    .addTestExecutionListeners(new LegacyXmlReportGeneratingListener(reportsDir, out))
    .addTestExecutionListeners(new CustomTestExecutionListener())
    .build();

LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
    .selectors(selectPackage("com.example.mytests"))
    .build();

try (LauncherSession session = LauncherFactory.openSession(launcherConfig)) {
    session.getLauncher().execute(request);
}

6.4.12. 干运行模式

通过 Launcher API 运行测试时,您可以通过将 junit.platform.execution.dryRun.enabled 配置参数 设置为 true 来启用干运行模式。在这种模式下,Launcher 不会实际执行任何测试,但会通知注册的 TestExecutionListener 实例,就好像所有测试都被跳过并且它们的容器已成功一样。这对于测试构建配置中的更改或验证监听器是否按预期调用很有用,而无需等待所有测试执行完成。

6.5. 测试引擎

TestEngine 为特定编程模型的测试发现执行提供便利。

例如,JUnit 提供了一个 TestEngine,它可以发现和执行使用 JUnit Jupiter 编程模型编写的测试(参见 编写测试扩展模型)。

6.5.1. JUnit 测试引擎

JUnit 提供了三个 TestEngine 实现。

6.5.2. 自定义测试引擎

您可以通过实现 junit-platform-engine 模块中的接口并注册您的引擎来贡献您自己的自定义 TestEngine

每个 TestEngine 必须提供自己的唯一 ID、从 EngineDiscoveryRequest发现测试,并根据 ExecutionRequest 执行这些测试。

junit- 唯一 ID 前缀保留给 JUnit 团队的 TestEngines

JUnit Platform Launcher 强制执行,只有 JUnit 团队发布的 TestEngine 实现才能在其 TestEngine ID 中使用 junit- 前缀。

  • 如果任何第三方 TestEngine 声称是 junit-jupiterjunit-vintage,则会抛出异常,立即停止 JUnit Platform 的执行。

  • 如果任何第三方 TestEngine 使用 junit- 前缀作为其 ID,则会记录警告消息。JUnit Platform 的后续版本将为这些违规行为抛出异常。

为了在启动 JUnit Platform 之前促进 IDE 和工具中的测试发现,鼓励 TestEngine 实现使用 @Testable 注释。例如,JUnit Jupiter 中的 @Test@TestFactory 注释使用 @Testable 进行元注释。有关更多详细信息,请参阅 @Testable 的 Javadoc。

如果您的自定义 TestEngine 需要配置,请考虑允许用户通过 配置参数 提供配置。但是请注意,强烈建议您为测试引擎支持的所有配置参数使用唯一的首字母缩略词。这样做将确保您的配置参数名称与其他测试引擎的配置参数名称之间没有冲突。此外,由于配置参数可以作为 JVM 系统属性提供,因此明智的做法是避免与其他系统属性的名称冲突。例如,JUnit Jupiter 使用 junit.jupiter. 作为其支持的所有配置参数的前缀。此外,与上面关于 TestEngine ID 的 junit- 前缀的警告一样,您不应使用 junit. 作为您自己配置参数名称的前缀。

虽然目前还没有关于如何实现自定义 TestEngine 的官方指南,但您可以参考 JUnit 测试引擎 的实现或 JUnit 5 wiki 中列出的第三方测试引擎的实现。您还将在互联网上找到各种教程和博客,演示如何编写自定义 TestEngine

HierarchicalTestEngineTestEngine SPI(由 junit-jupiter-engine 使用)的便捷抽象基实现,它只需要实现者提供测试发现的逻辑。它实现了 TestDescriptor 的执行,这些 TestDescriptor 实现 Node 接口,包括对并行执行的支持。

6.5.3. 注册 TestEngine

TestEngine 注册通过 Java 的 ServiceLoader 机制支持。

例如,junit-jupiter-engine 模块在 junit-jupiter-engine JAR 中的 /META-INF/services 文件夹中名为 org.junit.platform.engine.TestEngine 的文件中注册其 org.junit.jupiter.engine.JupiterTestEngine

6.5.4. 要求

本节中的“必须”、“不得”、“需要”、“应”、“不应”、“应该”、“不应该”、“推荐”、“可以”和“可选”等词语应按 RFC 2119 中的描述进行解释。
强制性要求

为了与构建工具和 IDE 互操作,TestEngine 实现必须遵守以下要求

  • TestEngine.discover() 返回的 TestDescriptor 必须TestDescriptor 实例树的根。这意味着不得在节点及其后代之间存在任何循环。

  • TestEngine 必须能够为其先前生成的任何唯一 ID(从 TestEngine.discover() 返回)发现 UniqueIdSelectors。这使得能够选择要执行或重新运行的测试子集。

  • 传递给 TestEngine.execute()EngineExecutionListenerexecutionSkippedexecutionStartedexecutionFinished 方法必须为从 TestEngine.discover() 返回的树中的每个 TestDescriptor 节点调用最多一次。父节点必须在子节点之前报告为已启动,并在子节点之后报告为已完成。如果节点报告为已跳过,则不得为其后代报告任何事件。

增强兼容性

遵守以下要求是可选的,但为了与构建工具和 IDE 增强兼容性,建议这样做

  • 除非要指示空发现结果,否则从 TestEngine.discover() 返回的 TestDescriptor 应该具有子节点,而不是完全动态的。这允许工具显示测试的结构并选择要执行的测试子集。

  • 在解析 UniqueIdSelectors 时,TestEngine 应该只返回具有匹配唯一 ID(包括其祖先)的 TestDescriptor 实例,但可以返回执行所选测试所需的额外同级或其他节点。

  • TestEngines 应该支持 标记 测试和容器,以便在发现测试时可以应用标记过滤器。

7. API 演变

JUnit 5 的主要目标之一是提高维护人员在 JUnit 被许多项目使用的情况下演变 JUnit 的能力。在 JUnit 4 中,最初作为内部构造添加的许多内容只被外部扩展编写者和工具构建者使用。这使得更改 JUnit 4 变得特别困难,有时甚至不可能。

这就是 JUnit 5 为所有公开可用的接口、类和方法引入定义的生命周期的原因。

7.1. API 版本和状态

每个发布的工件都有一个版本号 <major>.<minor>.<patch>,所有公开可用的接口、类和方法都使用来自 @API@API Guardian 项目进行注释。注释的 status 属性可以分配以下值之一。

状态 描述

内部

不得被除 JUnit 本身以外的任何代码使用。可能会在未经事先通知的情况下删除。

已弃用

不应再使用;可能会在下一次次要版本中消失。

实验性

适用于新的实验性功能,我们希望获得反馈。
谨慎使用此元素;它将来可能会被提升到MAINTAINEDSTABLE,但也可能在没有事先通知的情况下被删除,即使是在补丁中。

MAINTAINED

适用于在当前主要版本的下一个次要版本发布之前至少不会以向后不兼容的方式更改的功能。如果计划删除,它将首先降级为DEPRECATED

STABLE

适用于在当前主要版本(5.*)中不会以向后不兼容的方式更改的功能。

如果类型上存在@API注释,则认为它也适用于该类型的所有公共成员。成员允许声明更低稳定性的不同status值。

7.2. 实验性 API

下表列出了哪些 API 目前通过@API(status = EXPERIMENTAL)被指定为实验性。在依赖此类 API 时应谨慎。

包名称 类型名称

org.junit.jupiter.api

Timeout.ThreadMode (枚举)

5.9

org.junit.jupiter.api.extension

AnnotatedElementContext (接口)

5.10

org.junit.jupiter.api.extension

DynamicTestInvocationContext (接口)

5.8

org.junit.jupiter.api.extension

ExecutableInvoker (接口)

5.9

org.junit.jupiter.api.extension

TestInstancePreConstructCallback (接口)

5.9

org.junit.jupiter.api.io

CleanupMode (枚举)

5.9

org.junit.jupiter.api.io

TempDirFactory (接口)

5.10

org.junit.jupiter.params.converter

AnnotationBasedArgumentConverter (类)

5.10

org.junit.jupiter.params.provider

AnnotationBasedArgumentsProvider (类)

5.10

org.junit.platform.engine.discovery

IterationSelector (类)

1.9

org.junit.platform.engine.support.store

NamespacedHierarchicalStore (类)

5.10

org.junit.platform.engine.support.store

NamespacedHierarchicalStoreException (类)

5.10

org.junit.platform.jfr

FlightRecordingDiscoveryListener (类)

1.8

org.junit.platform.jfr

FlightRecordingExecutionListener (类)

1.8

org.junit.platform.launcher

LauncherDiscoveryListener (接口)

1.6

org.junit.platform.launcher

LauncherInterceptor (接口)

1.10

org.junit.platform.launcher

TestPlan.Visitor (接口)

1.10

org.junit.platform.launcher.listeners

UniqueIdTrackingListener (类)

1.8

org.junit.platform.reporting.open.xml

OpenTestReportGeneratingListener (类)

1.9

org.junit.platform.suite.api

SelectMethod (注释)

1.10

org.junit.platform.suite.api

SelectMethods (注释)

1.10

7.3. 已弃用的 API

下表列出了哪些 API 目前通过@API(status = DEPRECATED)被指定为已弃用。您应尽可能避免使用已弃用的 API,因为此类 API 可能会在即将发布的版本中删除。

包名称 类型名称

org.junit.jupiter.api

MethodOrderer.Alphanumeric (类)

5.7

org.junit.platform.commons.util

BlacklistedExceptions (类)

1.7

org.junit.platform.commons.util

PreconditionViolationException (类)

1.5

org.junit.platform.engine.support.filter

ClasspathScanningSupport (类)

1.5

org.junit.platform.engine.support.hierarchical

SingleTestExecutor (类)

1.2

org.junit.platform.launcher.listeners

LegacyReportingUtils (类)

1.6

org.junit.platform.runner

JUnitPlatform (类)

1.8

org.junit.platform.suite.api

UseTechnicalNames (注释)

1.8

7.4. @API 工具支持

The @API Guardian 项目计划为使用 @API 注释的 API 的发布者和使用者提供工具支持。例如,工具支持可能会提供一种方法来检查是否根据@API注释声明使用 JUnit API。

8. 贡献者

直接在 GitHub 上浏览 当前的贡献者列表

9. 发布说明

发布说明可在此处获得 此处

10. 附录

10.1. 可重现的构建

从 5.7 版本开始,JUnit 5 旨在使其非 javadoc JAR 成为 可重现的

在相同的构建条件下,例如 Java 版本,重复构建应提供相同的逐字节输出。

这意味着任何人都可以重现 Maven Central/Sonatype 上工件的构建条件,并在本地生成相同的输出工件,从而确认存储库中的工件实际上是从该源代码生成的。

10.2. 依赖项元数据

最终发布版和里程碑版的工件部署到 Maven Central,快照工件部署到 Sonatype 的 快照存储库,位于 /org/junit 下。

10.2.1. JUnit Platform

  • 组 ID: org.junit.platform

  • 版本: 1.10.2

  • 工件 ID:

    junit-platform-commons

    JUnit Platform 的通用 API 和支持实用程序。任何使用@API(status = INTERNAL)注释的 API 仅用于 JUnit 框架本身。外部方对内部 API 的任何使用都不受支持!

    junit-platform-console

    支持从控制台发现和执行 JUnit Platform 上的测试。有关详细信息,请参阅 控制台启动器

    junit-platform-console-standalone

    包含所有依赖项的可执行 JAR 在 Maven Central 的 junit-platform-console-standalone 目录下提供。有关详细信息,请参阅 控制台启动器

    junit-platform-engine

    测试引擎的公共 API。有关详细信息,请参阅 注册测试引擎

    junit-platform-jfr

    为 JUnit Platform 上的 Java Flight Recorder 事件提供LauncherDiscoveryListenerTestExecutionListener。有关详细信息,请参阅 Flight Recorder 支持

    junit-platform-launcher

    用于配置和启动测试计划的公共 API——通常由 IDE 和构建工具使用。有关详细信息,请参阅 JUnit Platform 启动器 API

    junit-platform-reporting

    生成测试报告的TestExecutionListener实现——通常由 IDE 和构建工具使用。有关详细信息,请参阅 JUnit Platform 报告

    junit-platform-runner

    用于在 JUnit 4 环境中执行 JUnit Platform 上的测试和测试套件的运行器。有关详细信息,请参阅 使用 JUnit 4 运行 JUnit Platform

    junit-platform-suite

    JUnit Platform Suite 工件,它传递依赖于junit-platform-suite-apijunit-platform-suite-engine,以便在 Gradle 和 Maven 等构建工具中简化依赖项管理。

    junit-platform-suite-api

    用于在 JUnit Platform 上配置测试套件的注释。由 JUnit Platform Suite EngineJUnitPlatform 运行器 支持。

    junit-platform-suite-commons

    在 JUnit Platform 上执行测试套件的通用支持实用程序。

    junit-platform-suite-engine

    在 JUnit Platform 上执行测试套件的引擎;仅在运行时需要。有关详细信息,请参阅 JUnit Platform Suite Engine

    junit-platform-testkit

    提供支持,用于为给定的TestEngine执行测试计划,然后通过流畅的 API 访问结果以验证预期结果。

10.2.2. JUnit Jupiter

  • 组 ID: org.junit.jupiter

  • 版本: 5.10.2

  • 工件 ID:

    junit-jupiter

    JUnit Jupiter 聚合工件,它传递依赖于junit-jupiter-apijunit-jupiter-paramsjunit-jupiter-engine,以便在 Gradle 和 Maven 等构建工具中简化依赖项管理。

    junit-jupiter-api

    JUnit Jupiter API,用于 编写测试扩展

    junit-jupiter-engine

    JUnit Jupiter 测试引擎实现;仅在运行时需要。

    junit-jupiter-params

    支持 JUnit Jupiter 中的 参数化测试

    junit-jupiter-migrationsupport

    支持从 JUnit 4 迁移到 JUnit Jupiter;仅在支持 JUnit 4 的@Ignore注释以及运行选定的 JUnit 4 规则时需要。

10.2.3. JUnit Vintage

  • 组 ID: org.junit.vintage

  • 版本: 5.10.2

  • 工件 ID:

    junit-vintage-engine

    JUnit Vintage 测试引擎实现,允许您在 JUnit Platform 上运行传统 JUnit 测试。传统测试包括使用 JUnit 3 或 JUnit 4 API 编写的测试,或者使用基于这些 API 的测试框架编写的测试。

10.2.4. 物料清单 (BOM)

使用以下 Maven 坐标提供的物料清单 POM 可以用于在使用 MavenGradle 引用多个上述工件时简化依赖项管理。

  • 组 ID: org.junit

  • 工件 ID: junit-bom

  • 版本: 5.10.2

10.2.5. 依赖项

上述大多数工件在其发布的 Maven POM 中都依赖于以下@API Guardian JAR。

  • 组 ID: org.apiguardian

  • 工件 ID: apiguardian-api

  • 版本: 1.1.2

此外,上述大多数工件都直接或间接依赖于以下OpenTest4J JAR。

  • 组 ID: org.opentest4j

  • 工件 ID: opentest4j

  • 版本: 1.3.0

10.3. 依赖项图

component diagram