1. 概述

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

本文档也以 PDF 下载 的形式提供。

1.1. 什么是 JUnit 5?

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

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

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

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

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。此类方法会被继承,除非被覆盖。

@AfterEach

表示带注解的方法应在当前类中的每个 @Test@RepeatedTest@ParameterizedTest@TestFactory 方法之后执行;类似于 JUnit 4 的 @After。此类方法会被继承,除非被覆盖。

@BeforeAll

表示带注解的方法应在当前类中的所有 @Test@RepeatedTest@ParameterizedTest@TestFactory 方法之前执行;类似于 JUnit 4 的 @BeforeClass。此类方法会被继承,除非被覆盖,并且必须是 static 的,除非使用“每个类”测试实例生命周期

@AfterAll

表示带注解的方法应在当前类中的所有 @Test@RepeatedTest@ParameterizedTest@TestFactory 方法之后执行;类似于 JUnit 4 的 @AfterClass。此类方法会被继承,除非被覆盖,并且必须是 static 的,除非使用“每个类”测试实例生命周期

@Nested

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

@Tag

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

@Disabled

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

@AutoClose

表示带注解的字段表示将在测试执行后自动关闭的资源。

@Timeout

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

@TempDir

用于在测试类构造函数、生命周期方法或测试方法中通过字段注入或参数注入提供临时目录;位于 org.junit.jupiter.api.io 包中。此类字段会被继承。

@ExtendWith

用于声明式注册扩展。此类注解会被继承。

@RegisterExtension

用于通过字段编程式注册扩展。此类字段会被继承。

某些注解目前可能是实验性的。有关详细信息,请查阅 实验性 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 的,并且必须具有单个构造函数。也支持 Java record 类。

测试方法

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

2.3. 测试类和方法

测试方法和生命周期方法可以在当前测试类本地声明,从超类继承,或从接口继承(参见测试接口和默认方法)。此外,测试方法和生命周期方法不得为 abstract,并且不得返回值(@TestFactory 方法除外,该方法需要返回值)。

类和方法可见性

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

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

字段和方法继承

测试类中的字段会被继承。例如,来自超类的 @TempDir 字段将始终应用于子类。

测试方法和生命周期方法会被继承,除非它们根据 Java 语言的可见性规则被覆盖。例如,来自超类的 @Test 方法将始终应用于子类,除非子类显式覆盖该方法。同样,如果在与子类不同的包中驻留的超类中声明了包私有 @Test 方法,则该 @Test 方法将始终应用于子类,因为子类无法覆盖来自不同包中的超类的包私有方法。

以下测试类演示了 @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() {
    }

}

也可以使用 Java record 类作为测试类,如下例所示。

作为 Java 记录编写的测试类
import static org.junit.jupiter.api.Assertions.assertEquals;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

record MyFirstJUnitJupiterRecordTests() {

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

}

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 方法。

断言方法可以选择接受断言消息作为其第三个参数,该参数可以是 StringSupplier<String>

当使用 Supplier<String>(例如,lambda 表达式)时,消息会被延迟评估。这可以提供性能优势,尤其是在消息构造复杂或耗时的情况下,因为它仅在断言失败时才会被评估。

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");

        // Lazily evaluates generateFailureMessage('a','b').
        assertTrue('a' < 'b', () -> generateFailureMessage('a','b'));
    }

    @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!";
    }

    private static String generateFailureMessage(char a, char b) {
        return "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily." + (a < b);
    }
}
使用 assertTimeoutPreemptively() 的抢占式超时

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

一个常见的例子是 Spring Framework 中的事务性测试支持。具体来说,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.assertInstanceOf
import org.junit.jupiter.api.assertNotNull
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)
        }
    }

    @Test
    fun `assertNotNull with a smart cast`() {
        val nullablePerson: Person? = person

        assertNotNull(nullablePerson)

        // The compiler smart casts nullablePerson to a non-nullable object.
        // The safe call operator (?.) isn't required.
        assertEquals(person.firstName, nullablePerson.firstName)
        assertEquals(person.lastName, nullablePerson.lastName)
    }

    @Test
    fun `assertInstanceOf with a smart cast`() {
        val maybePerson: Any = person

        assertInstanceOf<Person>(maybePerson)

        // The compiler smart casts maybePerson to a Person object,
        // allowing to access the Person properties.
        assertEquals(person.firstName, maybePerson.firstName)
        assertEquals(person.lastName, maybePerson.lastName)
    }
}

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. 假设

假设通常用于当继续执行给定测试没有意义时 — 例如,如果测试依赖于当前运行时环境中不存在的某些内容。

  • 当假设有效时,假设方法不会抛出异常,并且测试的执行会像往常一样继续。

  • 当假设无效时,假设方法会抛出 org.opentest4j.TestAbortedException 类型的异常,以表明测试应该中止而不是标记为失败。

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 4 的 org.junit.Assume 类中的方法进行假设。具体来说,JUnit Jupiter 支持 JUnit 4 的 AssumptionViolatedException 来表明测试应该中止而不是标记为失败。

2.7. 异常处理

JUnit Jupiter 提供了强大的异常处理支持。这包括用于管理由于异常导致的测试失败的内置机制、异常在实现断言和假设中的作用,以及如何专门断言代码中的非抛出条件。

2.7.1. 未捕获的异常

在 JUnit Jupiter 中,如果从测试方法、生命周期方法或扩展中抛出异常,并且该异常未在测试方法、生命周期方法或扩展中捕获,则框架会将测试或测试类标记为失败。

失败的假设偏离了这个一般规则。

与失败的断言相反,失败的假设不会导致测试失败;相反,失败的假设会导致测试中止。

有关更多详细信息和示例,请参阅 假设

在以下示例中,failsDueToUncaughtException() 方法抛出一个 ArithmeticException。由于异常未在测试方法中捕获,JUnit Jupiter 会将测试标记为失败。

private final Calculator calculator = new Calculator();

@Test
void failsDueToUncaughtException() {
    // The following throws an ArithmeticException due to division by
    // zero, which causes a test failure.
    calculator.divide(1, 0);
}
重要的是要注意,在测试方法中指定 throws 子句对测试结果没有影响。JUnit Jupiter 不会将 throws 子句解释为关于测试方法应该抛出哪些异常的期望或断言。只有当异常被意外抛出或断言失败时,测试才会失败。

2.7.2. 失败的断言

JUnit Jupiter 中的断言是使用异常实现的。框架在 org.junit.jupiter.api.Assertions 类中提供了一组断言方法,当断言失败时,这些方法会抛出 AssertionError。这种机制是 JUnit 如何处理作为异常的断言失败的核心方面。有关 JUnit Jupiter 断言支持的更多信息,请参阅 断言 部分。

第三方断言库可以选择抛出 AssertionError 来表示断言失败;但是,它们也可以选择抛出不同类型的异常来表示失败。另请参阅:第三方断言库
JUnit Jupiter 本身不区分失败的断言 (AssertionError) 和其他类型的异常。所有未捕获的异常都会导致测试失败。但是,集成开发环境 (IDE) 和其他工具可以通过检查抛出的异常是否是 AssertionError 的实例来区分这两种类型的失败。

在以下示例中,failsDueToUncaughtAssertionError() 方法抛出一个 AssertionError。由于异常未在测试方法中捕获,JUnit Jupiter 会将测试标记为失败。

private final Calculator calculator = new Calculator();

@Test
void failsDueToUncaughtAssertionError() {
    // The following incorrect assertion will cause a test failure.
    // The expected value should be 2 instead of 99.
    assertEquals(99, calculator.add(1, 1));
}

2.7.3. 断言预期的异常

JUnit Jupiter 提供了专门的断言,用于测试在预期条件下是否抛出了特定的异常。assertThrows()assertThrowsExactly() 断言是验证您的代码是否通过抛出适当的异常来正确响应错误情况的关键工具。

使用 assertThrows()

assertThrows() 方法用于验证在提供的可执行代码块的执行期间是否抛出了特定类型的异常。它不仅检查抛出的异常的类型,还检查其子类,使其适用于更通用的异常处理测试。assertThrows() 断言方法返回抛出的异常对象,以便对其执行其他断言。

@Test
void testExpectedExceptionIsThrown() {
    // The following assertion succeeds because the code under assertion
    // throws the expected IllegalArgumentException.
    // The assertion also returns the thrown exception which can be used for
    // further assertions like asserting the exception message.
    IllegalArgumentException exception =
        assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("expected message");
        });
    assertEquals("expected message", exception.getMessage());

    // The following assertion also succeeds because the code under assertion
    // throws IllegalArgumentException which is a subclass of RuntimeException.
    assertThrows(RuntimeException.class, () -> {
        throw new IllegalArgumentException("expected message");
    });
}
使用 assertThrowsExactly()

当您需要断言抛出的异常正是特定类型,而不允许预期异常类型的子类时,可以使用 assertThrowsExactly() 方法。当需要验证精确的异常处理行为时,这非常有用。与 assertThrows() 类似,assertThrowsExactly() 断言方法也返回抛出的异常对象,以便对其执行其他断言。

@Test
void testExpectedExceptionIsThrown() {
    // The following assertion succeeds because the code under assertion throws
    // IllegalArgumentException which is exactly equal to the expected type.
    // The assertion also returns the thrown exception which can be used for
    // further assertions like asserting the exception message.
    IllegalArgumentException exception =
        assertThrowsExactly(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("expected message");
        });
    assertEquals("expected message", exception.getMessage());

    // The following assertion fails because the assertion expects exactly
    // RuntimeException to be thrown, not subclasses of RuntimeException.
    assertThrowsExactly(RuntimeException.class, () -> {
        throw new IllegalArgumentException("expected message");
    });
}

2.7.4. 断言没有预期的异常

尽管从测试方法抛出的任何异常都会导致测试失败,但在某些用例中,显式断言给定的代码块抛出异常可能是有益的。当您想要验证特定的代码段是否未抛出任何异常时,可以使用 assertDoesNotThrow() 断言。

@Test
void testExceptionIsNotThrown() {
    assertDoesNotThrow(() -> {
        shouldNotThrowException();
    });
}

void shouldNotThrowException() {
}
第三方断言库通常也提供类似的支持。例如,AssertJ 具有 assertThatNoException().isThrownBy(() → …​)。另请参阅:第三方断言库

2.8. 禁用测试

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

@Disabled 应用于类级别时,该类中的所有测试方法也会自动禁用。

如果测试方法通过 @Disabled 禁用,则会阻止测试方法的执行和方法级别的生命周期回调,例如 @BeforeEach 方法、@AfterEach 方法和相应的扩展 API。但是,这不会阻止测试类被实例化,也不会阻止类级别的生命周期回调(例如 @BeforeAll 方法、@AfterAll 方法和相应的扩展 API)的执行。

这是一个 @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("在 bug #42 解决之前禁用")。一些开发团队甚至要求在原因中存在问题跟踪编号,以便进行自动追溯等。

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

2.9. 条件测试执行

JUnit Jupiter 中的 ExecutionCondition 扩展 API 允许开发人员基于某些条件以编程方式启用禁用测试类或测试方法。这种条件最简单的示例是内置的 DisabledCondition,它支持 @Disabled 注解(请参阅 禁用测试)。

除了 @Disabled 之外,JUnit Jupiter 还支持 org.junit.jupiter.api.condition 包中的其他几个基于注解的条件,这些条件允许开发人员声明式地启用或禁用测试类和测试方法。如果您希望提供有关它们可能被禁用的原因的详细信息,则与这些内置条件关联的每个注解都有一个可用于该目的的 disabledReason 属性。

当注册了多个 ExecutionCondition 扩展时,只要其中一个条件返回禁用,测试类或测试方法就会被禁用。如果测试类被禁用,则该类中的所有测试方法也会自动禁用。如果测试方法被禁用,则会阻止测试方法的执行和方法级别的生命周期回调,例如 @BeforeEach 方法、@AfterEach 方法和相应的扩展 API。但是,这不会阻止测试类被实例化,也不会阻止类级别的生命周期回调(例如 @BeforeAll 方法、@AfterAll 方法和相应的扩展 API)的执行。

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

组合注解

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

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

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

2.9.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.9.2. Java 运行时环境条件

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

以下列表演示了如何将这些注解与预定义的 JRE 枚举常量一起使用。

@Test
@EnabledOnJre(JAVA_17)
void onlyOnJava17() {
    // ...
}

@Test
@EnabledOnJre({ JAVA_17, JAVA_21 })
void onJava17And21() {
    // ...
}

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

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

@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 notOnJava9AndHigher() {
    // ...
}

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

由于 JRE 中定义的枚举常量对于任何给定的 JUnit 版本都是静态的,因此您可能会发现需要配置 JRE 枚举不支持的 Java 版本。例如,截至 JUnit Jupiter 5.12,JRE 枚举定义 JAVA_25 为最高支持的 Java 版本。但是,您可能希望针对更高版本的 Java 运行测试。为了支持此类用例,您可以通过 @EnabledOnJre@DisabledOnJre 中的 versions 属性以及 @EnabledForJreRange@DisabledForJreRange 中的 minVersionmaxVersion 属性指定任意 Java 版本。

以下列表演示了如何将这些注解与任意 Java 版本一起使用。

@Test
@EnabledOnJre(versions = 26)
void onlyOnJava26() {
    // ...
}

@Test
@EnabledOnJre(versions = { 25, 26 })
// Can also be expressed as follows.
// @EnabledOnJre(value = JAVA_25, versions = 26)
void onJava25And26() {
    // ...
}

@Test
@EnabledForJreRange(minVersion = 26)
void onJava26AndHigher() {
    // ...
}

@Test
@EnabledForJreRange(minVersion = 25, maxVersion = 27)
// Can also be expressed as follows.
// @EnabledForJreRange(min = JAVA_25, maxVersion = 27)
void fromJava25To27() {
    // ...
}

@Test
@DisabledOnJre(versions = 26)
void notOnJava26() {
    // ...
}

@Test
@DisabledOnJre(versions = { 25, 26 })
// Can also be expressed as follows.
// @DisabledOnJre(value = JAVA_25, versions = 26)
void notOnJava25And26() {
    // ...
}

@Test
@DisabledForJreRange(minVersion = 26)
void notOnJava26AndHigher() {
    // ...
}

@Test
@DisabledForJreRange(minVersion = 25, maxVersion = 27)
// Can also be expressed as follows.
// @DisabledForJreRange(min = JAVA_25, maxVersion = 27)
void notFromJava25To27() {
    // ...
}

2.9.3. Native Image 条件

可以通过 @EnabledInNativeImage@DisabledInNativeImage 注解在 GraalVM native image 中启用或禁用容器或测试。当使用来自 GraalVM Native Build Tools 项目的 Gradle 和 Maven 插件在 native image 中运行测试时,通常会使用这些注解。

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

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

2.9.4. 系统属性条件

可以通过 @EnabledIfSystemProperty@DisabledIfSystemProperty 注解基于 named JVM 系统属性的值启用或禁用容器或测试。通过 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.9.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.9.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.10. 标记和过滤

测试类和方法可以通过 @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.11. 测试执行顺序

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

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

2.11.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.11.2. 类顺序

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

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

  • 启用并行执行后,首先安排较长的测试:“最短测试计划执行持续时间”模式

  • 各种其他用例

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

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

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

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.12. 测试实例生命周期

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

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

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

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

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

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

2.12.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.13. 嵌套测试

@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 或更高版本,则可以将 @BeforeAll@AfterAll 方法声明为 @Nested 测试类中的 static 方法,并且此限制不再适用。

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

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

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

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

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

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

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

import org.junit.jupiter.api.BeforeAll;
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 {

    @BeforeAll
    static void beforeAll(TestInfo testInfo) {
        assertEquals("TestInfo Demo", testInfo.getDisplayName());
    }

    TestInfoDemo(TestInfo testInfo) {
        String displayName = testInfo.getDisplayName();
        assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
    }

    @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()fileEntryPublished() 方法使用。这允许它们在 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);
    }

    @Test
    void reportFiles(TestReporter testReporter, @TempDir Path tempDir) throws Exception {

        testReporter.publishFile("test1.txt", MediaType.TEXT_PLAIN_UTF_8,
            file -> Files.write(file, singletonList("Test 1")));

        Path existingFile = Files.write(tempDir.resolve("test2.txt"), singletonList("Test 2"));
        testReporter.publishFile(existingFile, MediaType.TEXT_PLAIN_UTF_8);

        testReporter.publishDirectory("test3", dir -> {
            Files.write(dir.resolve("nested1.txt"), singletonList("Nested content 1"));
            Files.write(dir.resolve("nested2.txt"), singletonList("Nested content 2"));
        });

        Path existingDir = Files.createDirectory(tempDir.resolve("test4"));
        Files.write(existingDir.resolve("nested1.txt"), singletonList("Nested content 1"));
        Files.write(existingDir.resolve("nested2.txt"), singletonList("Nested content 2"));
        testReporter.publishDirectory(existingDir);
    }
}
其他参数解析器必须通过使用 @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.15. 测试接口和默认方法

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();
        assertNotEquals(null, value);
    }

    @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.16. 重复测试

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.16.1. 重复测试示例

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

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

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

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

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

由于 beforeEach() 方法使用 @BeforeEach 注解,因此它将在每个重复测试的每次重复之前执行。通过将 TestInfoRepetitionInfo 注入到方法中,我们看到可以获取有关当前正在执行的重复测试的信息。启用 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.17. 参数化测试

参数化测试使您可以使用不同的参数多次运行测试。它们的声明方式与常规 @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.17.1. 必需的设置

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

2.17.2. 使用参数

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

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

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

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

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

AutoCloseable 参数

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

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

2.17.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 方法将被调用三次,值分别为 123

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

为了检查边界情况并验证我们的软件在提供错误输入时的正确行为,提供 null值给我们的参数化测试可能很有用。以下注解充当接受单个参数的参数化测试的 null 和空值的来源。

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

    • @NullSource 不能用于具有原始类型的参数。

  • @EmptySource:为以下类型的参数的带注解的 @ParameterizedTest 方法提供单个参数:java.lang.Stringjava.util.Collection(以及具有 public 无参数构造函数的具体子类型)、java.util.Listjava.util.Setjava.util.SortedSetjava.util.NavigableSetjava.util.Map(以及具有 public 无参数构造函数的具体子类型)、java.util.SortedMapjava.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));
}

除了 names 之外,您还可以使用 fromto 属性来指定常量范围。该范围从 from 属性中指定的常量开始,并包括所有后续常量,直到并包括 to 属性中指定的常量,基于枚举常量的自然顺序。

如果省略 fromto 属性,则它们分别默认为枚举类型中的第一个和最后一个常量。如果省略所有 namesfromto 属性,则将使用所有常量。以下示例演示了如何指定常量范围。

@ParameterizedTest
@EnumSource(from = "HOURS", to = "DAYS")
void testWithEnumSourceRange(ChronoUnit unit) {
    assertTrue(EnumSet.of(ChronoUnit.HOURS, ChronoUnit.HALF_DAYS, ChronoUnit.DAYS).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"));
}

您还可以将 modefromtonames 属性组合使用,以定义常量范围,同时从该范围中排除特定值,如下所示。

@ParameterizedTest
@EnumSource(from = "HOURS", to = "DAYS", mode = EXCLUDE, names = { "HALF_DAYS" })
void testWithEnumSourceRangeExclude(ChronoUnit unit) {
    assertTrue(EnumSet.of(ChronoUnit.HOURS, ChronoUnit.DAYS).contains(unit));
    assertFalse(EnumSet.of(ChronoUnit.HALF_DAYS).contains(unit));
}
@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;
    }

}
@FieldSource

@FieldSource 允许您引用测试类或外部类的一个或多个字段。

测试类中的字段必须是 static 的,除非测试类使用 @TestInstance(Lifecycle.PER_CLASS) 注解;而外部类中的字段必须始终是 static 的。

每个字段都必须能够提供参数流,并且“流”中的每组“参数”都将作为带注解的 @ParameterizedTest 方法的各个调用的物理参数提供。

在此上下文中,“流”是 JUnit 可以可靠地转换为 Stream 的任何内容;但是,实际的具体字段类型可以采用多种形式。一般来说,这转化为 CollectionIterable、流的 SupplierStreamDoubleStreamLongStreamIntStream)、IteratorSupplier、对象数组或原始数组。“流”中的每组“参数”可以作为 Arguments 的实例、对象数组(例如,Object[]String[] 等)提供,或者如果参数化测试方法接受单个参数,则可以作为单个值提供。

@MethodSource 工厂方法支持的返回类型相反,@FieldSource 字段的值不能是 StreamDoubleStreamLongStreamIntStreamIterator 的实例,因为这些类型的值在第一次处理时就被使用了。但是,如果您希望使用这些类型之一,则可以将其包装在 Supplier 中 — 例如,Supplier<IntStream>

请注意,作为一组“参数”提供的对象的一维数组将以不同于其他类型的参数的方式处理。具体来说,对象的一维数组的所有元素都将作为单独的物理参数传递给 @ParameterizedTest 方法。有关更多详细信息,请参阅 @FieldSource 的 Javadoc。

如果您没有通过 @FieldSource 显式提供字段名称,则 JUnit Jupiter 将按照约定在测试类中搜索与当前 @ParameterizedTest 方法同名的字段。以下示例对此进行了演示。此参数化测试方法将被调用两次:值分别为 "apple""banana"

@ParameterizedTest
@FieldSource
void arrayOfFruits(String fruit) {
    assertFruit(fruit);
}

static final String[] arrayOfFruits = { "apple", "banana" };

以下示例演示了如何通过 @FieldSource 提供单个显式字段名称。此参数化测试方法将被调用两次:值分别为 "apple""banana"

@ParameterizedTest
@FieldSource("listOfFruits")
void singleFieldSource(String fruit) {
    assertFruit(fruit);
}

static final List<String> listOfFruits = Arrays.asList("apple", "banana");

以下示例演示了如何通过 @FieldSource 提供多个显式字段名称。此示例使用了上一个示例中的 listOfFruits 字段以及 additionalFruits 字段。因此,此参数化测试方法将被调用四次:值分别为 "apple""banana""cherry""dewberry"

@ParameterizedTest
@FieldSource({ "listOfFruits", "additionalFruits" })
void multipleFieldSources(String fruit) {
    assertFruit(fruit);
}

static final Collection<String> additionalFruits = Arrays.asList("cherry", "dewberry");

只要流或迭代器包装在 java.util.function.Supplier 中,也可以通过 @FieldSource 字段提供 StreamDoubleStreamIntStreamLongStreamIterator 作为参数源。以下示例演示了如何提供命名参数的 StreamSupplier。此参数化测试方法将被调用两次:值分别为 "apple""banana",显示名称分别为 AppleBanana

@ParameterizedTest
@FieldSource
void namedArgumentsSupplier(String fruit) {
    assertFruit(fruit);
}

static final Supplier<Stream<Arguments>> namedArgumentsSupplier = () -> Stream.of(
    arguments(named("Apple", "apple")),
    arguments(named("Banana", "banana"))
);

请注意,arguments(Object…​) 是在 org.junit.jupiter.params.provider.Arguments 接口中定义的静态工厂方法。

同样,named(String, Object) 是在 org.junit.jupiter.api.Named 接口中定义的静态工厂方法。

如果参数化测试方法声明了多个参数,则相应的 @FieldSource 字段必须能够提供 Arguments 实例或对象数组的集合、流供应商或数组,如下所示(有关支持的类型的更多详细信息,请参阅 @FieldSource 的 Javadoc)。

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

static List<Arguments> stringIntAndListArguments = Arrays.asList(
    arguments("apple", 1, Arrays.asList("a", "b")),
    arguments("lemon", 2, Arrays.asList("x", "y"))
);

请注意,arguments(Object…​) 是在 org.junit.jupiter.params.provider.Arguments 接口中定义的静态工厂方法。

可以通过提供外部 static @FieldSource 字段的完全限定字段名来引用它,如下例所示。

@ParameterizedTest
@FieldSource("example.FruitUtils#tropicalFruits")
void testWithExternalFieldSource(String tropicalFruit) {
    // test with tropicalFruit
}
@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' 值。除非设置了 emptyValue 属性,否则空的带引号的值 ('') 会产生一个空的 String;而完全的值则被解释为 null 引用。通过指定一个或多个 nullValues,可以将自定义值解释为 null 引用(请参阅下表中的 NIL 示例)。如果 null 引用的目标类型是原始类型,则会抛出 ArgumentConversionException

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

默认情况下,除非在带引号的字符串中,否则 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" 值。除非设置了 emptyValue 属性,否则空的带引号的值 ("") 会产生一个空的 String;而完全的值则被解释为 null 引用。通过指定一个或多个 nullValues,可以将自定义值解释为 null 引用。如果 null 引用的目标类型是原始类型,则会抛出 ArgumentConversionException

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

默认情况下,除非在带引号的字符串中,否则 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 类。

此外,ArgumentsProvider 的实现可以声明构造函数参数,以防它们需要由注册的 ParameterResolver 解析,如下例所示。

public class MyArgumentsProviderWithConstructorInjection implements ArgumentsProvider {

    private final TestInfo testInfo;

    public MyArgumentsProviderWithConstructorInjection(TestInfo testInfo) {
        this.testInfo = testInfo;
    }

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(Arguments.of(testInfo.getDisplayName()));
    }
}
使用可重复注解的多个来源

可重复注解提供了一种方便的方式来指定来自不同提供程序的多个来源。

@DisplayName("A parameterized test that makes use of repeatable annotations")
@ParameterizedTest
@MethodSource("someProvider")
@MethodSource("otherProvider")
void testWithRepeatedAnnotation(String argument) {
    assertNotNull(argument);
}

static Stream<String> someProvider() {
    return Stream.of("foo");
}

static Stream<String> otherProvider() {
    return Stream.of("bar");
}

在上面的参数化测试之后,每个参数都将运行一个测试用例

[1] foo
[2] bar

以下注解是可重复的

  • @ValueSource

  • @EnumSource

  • @MethodSource

  • @FieldSource

  • @CsvSource

  • @CsvFileSource

  • @ArgumentsSource

2.17.4. 参数计数验证

参数计数验证目前是一项实验性功能。欢迎您试用并向 JUnit 团队提供反馈,以便他们可以改进并最终推广此功能。

默认情况下,当参数来源提供的参数多于测试方法需要的参数时,这些额外的参数将被忽略,测试照常执行。这可能会导致错误,其中参数永远不会传递给参数化测试方法。

为了防止这种情况,您可以将参数计数验证设置为“strict”。然后,任何额外的参数都将导致错误。

要更改所有测试的此行为,请将 junit.jupiter.params.argumentCountValidation 配置参数 设置为 strict。要更改单个测试的此行为,请使用 @ParameterizedTest 注解的 argumentCountValidation 属性

@ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.STRICT)
@CsvSource({ "42, -666" })
void testWithArgumentCountValidation(int number) {
    assertTrue(number > 0);
}

2.17.5. 参数转换

拓宽转换

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.cn/"URI.create("https://junit.cn/")

java.net.URL

"https://junit.cn/"URI.create("https://junit.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")

回退 String 到 Object 的转换

除了从字符串到上表列出的目标类型的隐式转换之外,如果目标类型声明了恰好一个合适的工厂方法工厂构造函数(如下定义),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;
    }
}
显式转换

您可以显式指定一个 ArgumentConverter 用于某个参数,而不是依赖隐式参数转换,方法是使用 @ConvertWith 注解,如下例所示。请注意,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.17.6. 参数聚合

默认情况下,提供给 @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 接口,并通过 @AggregateWith 注解在 @ParameterizedTest 方法中的兼容参数上注册它。然后,聚合的结果将作为参数化测试被调用时相应参数的参数提供。请注意,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.17.7. 自定义显示名称

默认情况下,参数化测试调用的显示名称包含调用索引和该特定调用的所有参数的 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}

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

{argumentSetName}

参数集的名称

{argumentSetNameOrArgumentsWithNames}

{argumentSetName}{argumentsWithNames},取决于参数的提供方式

{0}{1}、…​

单个参数

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

当使用 @MethodSource@FieldSource@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")))
    );
}

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

A parameterized test with named arguments ✔
├─ 1: An important file ✔
└─ 2: Another file ✔

请注意,arguments(Object…​) 是在 org.junit.jupiter.params.provider.Arguments 接口中定义的静态工厂方法。

同样,named(String, Object) 是在 org.junit.jupiter.api.Named 接口中定义的静态工厂方法。

使用 ArgumentSet API 为整组参数提供自定义名称,自定义名称将用作显示名称,如下例所示。

@DisplayName("A parameterized test with named argument sets")
@ParameterizedTest
@FieldSource("argumentSets")
void testWithArgumentSets(File file1, File file2) {
}

static List<Arguments> argumentSets = Arrays.asList(
    argumentSet("Important files", new File("path1"), new File("path2")),
    argumentSet("Other files", new File("path3"), new File("path4"))
);

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

A parameterized test with named argument sets ✔
├─ [1] Important files ✔
└─ [2] Other files ✔

请注意,argumentSet(String, Object…​) 是在 org.junit.jupiter.params.provider.Arguments 接口中定义的静态工厂方法。

如果您想为项目中的所有参数化测试设置默认名称模式,您可以在 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.17.8. 生命周期和互操作性

参数化测试的每次调用都具有与常规 @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.18. 测试模板

@TestTemplate 方法不是常规测试用例,而是测试用例的模板。因此,它旨在根据注册提供程序返回的调用上下文的数量多次调用。因此,它必须与注册的 TestTemplateInvocationContextProvider 扩展一起使用。测试模板方法的每次调用都像执行常规 @Test 方法一样,完全支持相同的生命周期回调和扩展。有关用法示例,请参阅为测试模板提供调用上下文

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

2.19. 动态测试

注解中描述的 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 方法生成的各个动态测试之间由回调方法或扩展重置。

2.19.1. 动态测试示例

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

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

接下来的六个方法演示了 DynamicTest 实例的 CollectionIterableIterator、数组或 Stream 的生成。这些示例大多数并没有真正展示动态行为,而只是原则上演示了支持的返回类型。但是,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 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.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, () -> assertEquals(0, n % 2)));
    }

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTests() {

        // 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<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.19.2. 动态测试和 Named

在某些情况下,使用 Named API 和 DynamicTest 上的相应 stream() 工厂方法来指定带有描述性名称的输入可能更自然,如下面的第一个示例所示。第二个示例更进一步,允许通过实现 Executable 接口以及通过 NamedExecutable 基类实现 Named 来提供应执行的代码块。

import static example.util.StringUtils.isPalindrome;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Named.named;

import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.NamedExecutable;
import org.junit.jupiter.api.TestFactory;

public class DynamicTestsNamedDemo {

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
        // Stream of palindromes to check
        var 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<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNamedExecutables() {
        // Stream of palindromes to check
        var inputStream = Stream.of("racecar", "radar", "mom", "dad")
                .map(PalindromeNamedExecutable::new);

        // Returns a stream of dynamic tests based on NamedExecutables.
        return DynamicTest.stream(inputStream);
    }

    record PalindromeNamedExecutable(String text) implements NamedExecutable {

        @Override
        public String getName() {
            return String.format("'%s' is a palindrome", text);
        }

        @Override
        public void execute() {
            assertTrue(isPalindrome(text));
        }
    }
}

2.19.3. 动态测试的 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 的 Javadoc。

ClassSource

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

UriSource

如果以上 TestSource 实现均不适用。

2.20. 超时

@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.20.1. 线程模式

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

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

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

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

2.20.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.default

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

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

42

@Timeout(42)

42 纳秒

@Timeout(value = 42, unit = NANOSECONDS)

42 微秒

@Timeout(value = 42, unit = MICROSECONDS)

42 毫秒

@Timeout(value = 42, unit = MILLISECONDS)

42 秒

@Timeout(value = 42, unit = SECONDS)

42 分钟

@Timeout(value = 42, unit = MINUTES)

42 小时

@Timeout(value = 42, unit = HOURS)

42 天

@Timeout(value = 42, unit = DAYS)

2.20.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.20.4. 调试超时

在对超时方法执行的线程调用 Thread.interrupt() 之前,会调用已注册的 Pre-Interrupt Callback 扩展。这允许检查应用程序状态并输出可能有助于诊断超时原因的附加信息。

超时时的线程转储

JUnit 注册了 Pre-Interrupt Callback 扩展点的默认实现,如果通过将 junit.jupiter.execution.timeout.threaddump.enabled 配置参数 设置为 true 启用,则会将所有线程的堆栈转储到 System.out

2.20.5. 全局禁用 @Timeout

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

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

2.21. 并行执行

默认情况下,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 的测试类。在前一种情况下,测试作者必须确保测试类是线程安全的;在后一种情况下,并发执行可能会与配置的执行顺序冲突。因此,在这两种情况下,只有当测试类或方法上存在 @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

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

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

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

2.21.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 将使用因子为 1dynamic 配置策略。因此,所需的并行度将等于可用处理器/核心数。

并行度本身并不意味着最大并发线程数
默认情况下,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

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

正十进制数,必须大于或等于 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.21.2. 同步

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

除了静态声明这些共享资源外,@ResourceLock 注解还具有 providers 属性,该属性允许注册 ResourceLocksProvider 接口的实现,该接口可以在运行时动态添加共享资源。请注意,使用 @ResourceLock 注解静态声明的资源与 ResourceLocksProvider 实现动态添加的资源相结合。

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

当使用 @ResourceLock 注解声明对共享资源的访问时,JUnit Jupiter 引擎会使用此信息来确保没有冲突的测试并行运行。此保证扩展到测试类或方法的生命周期方法。例如,如果测试方法使用 @ResourceLock 注解进行注解,则将在执行任何 @BeforeEach 方法之前获取“锁”,并在执行完所有 @AfterEach 方法之后释放“锁”。

隔离运行测试

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

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

使用 @ResourceLock 注解“静态”声明共享资源
@Execution(CONCURRENT)
class StaticSharedResourcesDemo {

    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"));
    }

}
使用 ResourceLocksProvider 实现“动态”添加共享资源
@Execution(CONCURRENT)
@ResourceLock(providers = DynamicSharedResourcesDemo.Provider.class)
class DynamicSharedResourcesDemo {

    private Properties backup;

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

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

    @Test
    void customPropertyIsNotSetByDefault() {
        assertNull(System.getProperty("my.prop"));
    }

    @Test
    void canSetCustomPropertyToApple() {
        System.setProperty("my.prop", "apple");
        assertEquals("apple", System.getProperty("my.prop"));
    }

    @Test
    void canSetCustomPropertyToBanana() {
        System.setProperty("my.prop", "banana");
        assertEquals("banana", System.getProperty("my.prop"));
    }

    static class Provider implements ResourceLocksProvider {

        @Override
        public Set<Lock> provideForMethod(List<Class<?>> enclosingInstanceTypes, Class<?> testClass,
                Method testMethod) {
            ResourceAccessMode mode = testMethod.getName().startsWith("canSet") ? READ_WRITE : READ;
            return Collections.singleton(new Lock(SYSTEM_PROPERTIES, mode));
        }
    }

}

此外,也可以通过 @ResourceLock 注解中的 target 属性为直接子节点声明“静态”共享资源,该属性接受来自 ResourceLockTarget 枚举的值。

在类级别的 @ResourceLock 注解中指定 target = CHILDREN 具有与为此类中声明的每个测试方法和嵌套测试类添加具有相同 valuemode 的注解相同的语义。

当测试类声明 READ 锁,但只有少数方法持有 READ_WRITE 锁时,这可能会提高并行化。

如果在 @ResourceLock 没有 target = CHILDREN 的情况下,以下示例中的测试将在 SAME_THREAD 中运行。这是因为测试类声明了 READ 共享资源,但一个测试方法持有 READ_WRITE 锁,这将强制所有测试方法采用 SAME_THREAD 执行模式。

使用 target 属性为子节点声明共享资源
@Execution(CONCURRENT)
@ResourceLock(value = "a", mode = READ, target = CHILDREN)
public class ChildrenSharedResourcesDemo {

    @ResourceLock(value = "a", mode = READ_WRITE)
    @Test
    void test1() throws InterruptedException {
        Thread.sleep(2000L);
    }

    @Test
    void test2() throws InterruptedException {
        Thread.sleep(2000L);
    }

    @Test
    void test3() throws InterruptedException {
        Thread.sleep(2000L);
    }

    @Test
    void test4() throws InterruptedException {
        Thread.sleep(2000L);
    }

    @Test
    void test5() throws InterruptedException {
        Thread.sleep(2000L);
    }

}

2.22. 内置扩展

虽然 JUnit 团队鼓励将可重用扩展打包并在单独的库中维护,但 JUnit Jupiter 包含了一些面向用户的扩展实现,这些实现被认为非常通用,用户无需添加其他依赖项。

2.22.1. @TempDir 扩展

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

例如,以下测试为单个测试方法声明了一个使用 @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。但是,请注意,此选项已弃用,将在未来版本中删除。

以下示例在 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 创建的。

以下示例定义了一个工厂,该工厂使用测试名称作为目录名称前缀,而不是 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 注解的字段或参数上的元注解或其他注解可能会公开其他属性来配置工厂。可以通过 createTempDirectory(…​) 方法的 AnnotatedElementContext 参数访问此类注解和相关属性。

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

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

  1. @TempDir 注解的 factory 属性(如果存在)

  2. 通过配置参数配置的默认 TempDirFactory(如果存在)

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

2.22.2. @AutoClose 扩展

内置的 AutoCloseExtension 自动关闭与字段关联的资源。默认情况下已注册。要使用它,请使用 @AutoClose 注解测试类中的字段。

@AutoClose 字段可以是 static 或非 static 的。如果在评估 @AutoClose 字段的值时该字段为 null,则该字段将被忽略,但会记录警告消息以通知您。

默认情况下,@AutoClose 期望带注解的字段的值实现一个 close() 方法,该方法将被调用以关闭资源。但是,开发人员可以通过 value 属性自定义 close 方法的名称。例如,@AutoClose("shutdown") 指示 JUnit 查找 shutdown() 方法以关闭资源。

@AutoClose 字段从超类继承。此外,子类中的 @AutoClose 字段将在超类中的 @AutoClose 字段之前关闭。

当给定的测试类中存在多个 @AutoClose 字段时,资源关闭的顺序取决于确定性的但有意不明显的算法。这确保了测试套件的后续运行以相同的顺序关闭资源,从而允许可重复构建。

AutoCloseExtension 实现了 AfterAllCallbackTestInstancePreDestroyCallback 扩展 API。因此,static @AutoClose 字段将在当前测试类中的所有测试完成后关闭,实际上是在为测试类执行 @AfterAll 方法之后。非 static 的 @AutoClose 字段将在销毁当前测试类实例之前关闭。具体而言,如果测试类配置了 @TestInstance(Lifecycle.PER_METHOD) 语义,则非 static 的 @AutoClose 字段将在执行每个测试方法、测试工厂方法或测试模板方法之后关闭。但是,如果测试类配置了 @TestInstance(Lifecycle.PER_CLASS) 语义,则非 static 的 @AutoClose 字段将不会关闭,直到不再需要当前测试类实例,这意味着在 @AfterAll 方法之后以及所有 static @AutoClose 字段已关闭之后。

以下示例演示了如何使用 @AutoClose 注解实例字段,以便在测试执行后自动关闭资源。在此示例中,我们假设应用默认的 @TestInstance(Lifecycle.PER_METHOD) 语义。

使用 @AutoClose 关闭资源的测试类
class AutoCloseDemo {

    @AutoClose (1)
    WebClient webClient = new WebClient(); (2)

    String serverUrl = // specify server URL ...

    @Test
    void getProductList() {
        // Use WebClient to connect to web server and verify response
        assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
    }

}
1 使用 @AutoClose 注解实例字段。
2 WebClient 实现了 java.lang.AutoCloseable,后者定义了一个 close() 方法,该方法将在每个 @Test 方法之后调用。

3. 从 JUnit 4 迁移

尽管 JUnit Jupiter 编程模型和扩展模型本身不支持 JUnit 4 的功能(如 RulesRunners),但源代码维护者无需更新其所有现有测试、测试扩展和自定义构建测试基础结构即可迁移到 JUnit Jupiter。

相反,JUnit 通过 JUnit Vintage 测试引擎 提供了一种平稳的迁移路径,该引擎允许使用 JUnit Platform 基础结构执行基于 JUnit 3 和 JUnit 4 的现有测试。由于特定于 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 Platform 启动器将自动拾取 JUnit 3 和 JUnit 4 测试。

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

3.1.1. 类别支持

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

3.2. 并行执行

JUnit Vintage 测试引擎支持并行执行顶层测试类和测试方法,从而允许现有 JUnit 3 和 JUnit 4 测试通过并发测试执行从改进的性能中受益。可以使用以下 配置参数 启用和配置它

junit.vintage.execution.parallel.enabled=true|false

启用/禁用并行执行(默认为 false)。需要选择加入才能使用以下配置参数并行执行 classesmethods

junit.vintage.execution.parallel.classes=true|false

启用/禁用并行执行测试类(默认为 false)。

junit.vintage.execution.parallel.methods=true|false

启用/禁用并行执行测试方法(默认为 false)。

junit.vintage.execution.parallel.pool-size=<number>

指定用于并行执行的线程池的大小。默认情况下,使用可用处理器的数量。

junit-platform.properties 中的配置示例

junit.vintage.execution.parallel.enabled=true
junit.vintage.execution.parallel.classes=true
junit.vintage.execution.parallel.methods=true
junit.vintage.execution.parallel.pool-size=4

设置这些属性后,VintageTestEngine 将并行执行测试,从而可能显着减少整体测试套件执行时间。

3.3. 迁移技巧

以下是将现有 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 rule 不再存在;请改用 Assertions.assertThrows(…​)

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

3.4. 有限的 JUnit 4 Rule 支持

如上所述,JUnit Jupiter 不支持也不会原生支持 JUnit 4 rules。但是,JUnit 团队意识到,许多组织(尤其是大型组织)可能拥有大量使用自定义 rules 的 JUnit 4 代码库。为了服务于这些组织并实现平稳的迁移路径,JUnit 团队已决定在 JUnit Jupiter 中逐字支持精选的 JUnit 4 rules。此支持基于适配器,并且仅限于那些在语义上与 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 启用这种有限形式的 Rule 支持。此注解是一个组合注解,它启用所有 rule 迁移支持扩展:VerifierSupportExternalResourceSupportExpectedExceptionSupport。您可以选择使用 @EnableJUnit4MigrationSupport 注解您的测试类,这将注册对 rules JUnit 4 的 @Ignore 注解的迁移支持(请参阅 JUnit 4 @Ignore 支持)。

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

3.5. JUnit 4 @Ignore 支持

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

要将 @Ignore 与基于 JUnit Jupiter 的测试一起使用,请在你的构建中配置对 junit-jupiter-migrationsupport 模块的测试依赖,然后使用 @ExtendWith(IgnoreCondition.class)@EnableJUnit4MigrationSupport 注解你的测试类(它会自动注册 IgnoreCondition 以及 有限的 JUnit 4 Rule 支持)。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.6. 失败消息参数

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)。这样做的理由是失败消息是可选的,可选参数应在方法签名中的必需参数之后声明。

受此更改影响的方法如下

  • 断言 (Assertions)

    • assertTrue

    • assertFalse

    • assertNull

    • assertNotNull

    • assertEquals

    • assertNotEquals

    • assertArrayEquals

    • assertSame

    • assertNotSame

    • assertThrows

  • 假设 (Assumptions)

    • assumeTrue

    • assumeFalse

4. 运行测试

4.1. IDE 支持

4.1.1. IntelliJ IDEA

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

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

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

额外的 Gradle 依赖
testImplementation(platform("org.junit:junit-bom:5.12.0"))
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.12.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

4.1.2. Eclipse

自 Eclipse Oxygen.1a (4.7.1a) 版本起,Eclipse IDE 提供对 JUnit Platform 的支持。

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

4.1.3. NetBeans

Apache NetBeans 10.0 版本起,NetBeans 提供对 JUnit Jupiter 和 JUnit Platform 的支持。

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

4.1.4. Visual Studio Code

Visual Studio Code 通过 Java Test Runner 扩展(默认安装为 Java Extension Pack 的一部分)支持 JUnit Jupiter 和 JUnit Platform。

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

4.1.5. 其他 IDE

如果你使用的编辑器或 IDE 不是前面部分列出的那些,JUnit 团队提供了两种替代解决方案来帮助你使用 JUnit 5。你可以手动使用 Console Launcher — 例如,从命令行 — 或者如果你的 IDE 内置支持 JUnit 4,则可以使用基于 JUnit 4 的 Runner 执行测试。

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.12.0"))
}

使用 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.12.0") // version can be omitted when using the BOM
}

JUnit Platform 可以运行基于 JUnit 4 的测试,前提是你配置对 JUnit 4 的 testImplementation 依赖,以及对 JUnit Vintage TestEngine 实现的 testRuntimeOnly 依赖,类似于以下内容。

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

JUnit 在 java.util.logging 包(又名 JUL)中使用 Java Logging API 来发出警告和调试信息。有关配置选项,请参阅 LogManager 的官方文档。

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

test {
    systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
    // Avoid overhead (see https://logging.apache.ac.cn/log4j/2.x/manual/jmx.html#enabling-jmx)
    systemProperty("log4j2.disableJmx", "true")
}

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

4.2.2. Maven

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

使用最新版本的 Maven Surefire/Failsafe 以避免互操作性问题

为了避免互操作性问题,建议使用最新版本的 Maven Surefire/Failsafe(3.0.0 或更高版本),因为它会自动对齐使用的 JUnit Platform Launcher 的版本与在测试运行时类路径上找到的 JUnit Platform 版本。

如果你使用的是低于 3.0.0-M4 的版本,你可以通过在 Maven 构建中添加对匹配版本的 JUnit Platform Launcher 的测试依赖来解决缺少对齐的问题,如下所示。

<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.12.0</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.12.0</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.12.0</version> <!-- can be omitted when using the BOM -->
        <scope>test</scope>
    </dependency>
    <!-- ... -->
</dependencies>
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>3.5.2</version>
        </plugin>
    </plugins>
</build>
<!-- ... -->

Maven Surefire 和 Maven Failsafe 可以与 Jupiter 测试一起运行基于 JUnit 4 的测试,前提是你配置对 JUnit 4 的 test 作用域的依赖,以及对 JUnit Vintage TestEngine 实现的依赖,类似于以下内容。

<!-- ... -->
<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.12.0</version> <!-- can be omitted when using the BOM -->
        <scope>test</scope>
    </dependency>
    <!-- ... -->
</dependencies>
<!-- ... -->
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>3.5.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.5.2</version>
            <configuration>
                <excludes>
                    <exclude/>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>
<!-- ... -->

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

按标签过滤

你可以使用以下配置属性按 标签标签表达式 过滤测试。

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

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

<!-- ... -->
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.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.5.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 中 fork 测试

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。Gradle 和 Maven 都记录了更改依赖项版本的机制。

使用 Gradle,你可以通过在 build.gradle 文件中包含以下内容来覆盖 JUnit Jupiter 版本。

ext['junit-jupiter.version'] = '5.12.0'

使用 Maven,你可以通过在 pom.xml 文件中包含以下内容来覆盖 JUnit Jupiter 版本。

<properties>
    <junit-jupiter.version>5.12.0</junit-jupiter.version>
</properties>

4.3. Console Launcher

ConsoleLauncher 是一个命令行 Java 应用程序,可让你从控制台启动 JUnit Platform。例如,它可以用于运行 JUnit Vintage 和 JUnit Jupiter 测试,并将测试执行结果打印到控制台。

包含所有依赖项内容的 Fat JAR (junit-platform-console-standalone-1.12.0.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.12.0

  • org.junit.jupiter:junit-jupiter-engine:5.12.0

  • org.junit.jupiter:junit-jupiter-params:5.12.0

  • org.junit.platform:junit-platform-commons:1.12.0

  • org.junit.platform:junit-platform-console:1.12.0

  • org.junit.platform:junit-platform-engine:1.12.0

  • org.junit.platform:junit-platform-launcher:1.12.0

  • org.junit.platform:junit-platform-reporting:1.12.0

  • org.junit.platform:junit-platform-suite-api:1.12.0

  • org.junit.platform:junit-platform-suite-commons:1.12.0

  • org.junit.platform:junit-platform-suite-engine:1.12.0

  • org.junit.vintage:junit-vintage-engine:5.12.0

  • org.opentest4j:opentest4j:1.3.0

由于 junit-platform-console-standalone JAR 包含其所有依赖项的内容,因此其 Maven POM 未声明任何依赖项。

此外,你不太可能需要在项目的 Maven POM 或 Gradle 构建脚本中包含对 junit-platform-console-standalone 组件的依赖。相反,可执行的 junit-platform-console-standalone JAR 通常直接从命令行或 shell 脚本调用,而无需构建脚本。

如果你需要在构建脚本中声明对 junit-platform-console-standalone 组件中包含的某些组件的依赖,则应仅声明对项目中使用的 JUnit 组件的依赖。为了简化构建中 JUnit 组件的依赖管理,你可能希望使用 junit-jupiter 聚合器组件或 junit-bom。有关详细信息,请参阅 依赖元数据

你可以 运行 独立的 ConsoleLauncher,如下所示。

$ java -jar junit-platform-console-standalone-1.12.0.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.
  -h, --help             Display help information.
      --version          Display version information.
      --disable-ansi-colors
                         Disable ANSI colors in output (not supported by all terminals).
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.cn/junit5/docs/1.12.0/user-guide/
发现测试
Usage: junit discover [OPTIONS]
Discover tests
      [@<filename>...]       One or more argument files containing options.
      --disable-ansi-colors  Disable ANSI colors in output (not supported by all terminals).
      --disable-banner       Disable print out of the welcome message.
  -h, --help                 Display help information.
      --version              Display version 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. The line and column numbers can
                               be provided as URI query parameters (e.g. foo.txt?
                               line=12&column=34). 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=PREFIX:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]...
                             Select iterations for test discovery via a prefixed identifier
                               and a list of indexes or index ranges (e.g. method:com.acme.
                               Foo#m()[1..2] selects the first and second iteration of the m()
                               method in the com.acme.Foo class). This option can be repeated.
      --uid, --select-unique-id=UNIQUE-ID...
                             Select a unique id for test discovery. This option can be
                               repeated.
      --select=PREFIX:VALUE...
                             Select via a prefixed identifier (e.g. method:com.acme.Foo#m
                               selects the m() method in the com.acme.Foo class). This option
                               can be repeated.

  For more information on selectors including syntax examples, see
  https://junit.cn/junit5/docs/current/user-guide/#running-tests-discovery-selectors

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.
      --include-methodname=PATTERN
                             Provide a regular expression to include only methods whose fully
                               qualified names without parameters match. When this option is
                               repeated, all patterns will be combined using OR semantics.
      --exclude-methodname=PATTERN
                             Provide a regular expression to exclude those methods whose fully
                               qualified names without parameters match. When this option is
                               repeated, all patterns will be combined using OR semantics.
  -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-resource=PATH Set configuration parameters for test discovery and execution via
                               a classpath resource. 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.cn/junit5/docs/1.12.0/user-guide/
执行测试
Usage: junit execute [OPTIONS]
Execute tests
      [@<filename>...]       One or more argument files containing options.
      --disable-ansi-colors  Disable ANSI colors in output (not supported by all terminals).
      --disable-banner       Disable print out of the welcome message.
  -h, --help                 Display help information.
      --version              Display version 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. The line and column numbers can
                               be provided as URI query parameters (e.g. foo.txt?
                               line=12&column=34). 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=PREFIX:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]...
                             Select iterations for test discovery via a prefixed identifier
                               and a list of indexes or index ranges (e.g. method:com.acme.
                               Foo#m()[1..2] selects the first and second iteration of the m()
                               method in the com.acme.Foo class). This option can be repeated.
      --uid, --select-unique-id=UNIQUE-ID...
                             Select a unique id for test discovery. This option can be
                               repeated.
      --select=PREFIX:VALUE...
                             Select via a prefixed identifier (e.g. method:com.acme.Foo#m
                               selects the m() method in the com.acme.Foo class). This option
                               can be repeated.

  For more information on selectors including syntax examples, see
  https://junit.cn/junit5/docs/current/user-guide/#running-tests-discovery-selectors

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.
      --include-methodname=PATTERN
                             Provide a regular expression to include only methods whose fully
                               qualified names without parameters match. When this option is
                               repeated, all patterns will be combined using OR semantics.
      --exclude-methodname=PATTERN
                             Provide a regular expression to exclude those methods whose fully
                               qualified names without parameters match. When this option is
                               repeated, all patterns will be combined using OR semantics.
  -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-resource=PATH Set configuration parameters for test discovery and execution via
                               a classpath resource. 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.cn/junit5/docs/1.12.0/user-guide/
列出测试引擎
Usage: junit engines [OPTIONS]
List available test engines
      [@<filename>...]   One or more argument files containing options.
      --disable-ansi-colors
                         Disable ANSI colors in output (not supported by all terminals).
      --disable-banner   Disable print out of the welcome message.
  -h, --help             Display help information.
      --version          Display version information.

For more information, please refer to the JUnit User Guide at
https://junit.cn/junit5/docs/1.12.0/user-guide/

4.3.2. 参数文件 (@-files)

在某些平台上,当创建包含大量选项或长参数的命令行时,你可能会遇到系统对命令行长度的限制。

自 1.3 版本起,ConsoleLauncher 支持参数文件,也称为 @-files。参数文件是本身包含要传递给命令的参数的文件。当底层 picocli 命令行解析器遇到以字符 @ 开头的参数时,它会将该文件的内容扩展到参数列表中。

文件中的参数可以用空格或换行符分隔。如果参数包含嵌入的空格,则应将整个参数用双引号或单引号括起来 — 例如,"-f=My Files/Stuff.java"

如果参数文件不存在或无法读取,则该参数将被按字面意思处理,并且不会被删除。这很可能会导致“unmatched argument”错误消息。你可以通过将 picocli.trace 系统属性设置为 DEBUG 来排查此类错误。

可以在命令行上指定多个 @-files。指定的路径可以是相对于当前目录的路径,也可以是绝对路径。

你可以通过使用额外的 @ 符号转义来传递带有初始 @ 字符的真实参数。例如,@@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 runner 已被弃用

JUnitPlatform runner 由 JUnit 团队开发,作为在 JUnit 4 环境中运行 JUnit Platform 上的测试套件和测试的临时解决方案。

近年来,所有主流构建工具和 IDE 都提供了内置支持,可以直接在 JUnit Platform 上运行测试。

此外,由 junit-platform-suite-engine 模块提供的 @Suite 支持的引入使 JUnitPlatform runner 变得过时。有关详细信息,请参阅 JUnit Platform Suite Engine

因此,JUnitPlatform runner 和 @UseTechnicalNames 注解已在 JUnit Platform 1.8 中被弃用,并将于 JUnit Platform 2.0 中移除。

如果你正在使用 JUnitPlatform runner,请迁移到 @Suite 支持。

JUnitPlatform runner 是一个基于 JUnit 4 的 Runner,它使你能够在 JUnit 4 环境中运行任何编程模型在 JUnit Platform 上受支持的测试 — 例如,JUnit Jupiter 测试类。

使用 @RunWith(JUnitPlatform.class) 注解类允许它与支持 JUnit 4 但尚不支持 JUnit Platform 的 IDE 和构建系统一起运行。

由于 JUnit Platform 具有 JUnit 4 没有的功能,因此 runner 只能支持 JUnit Platform 功能的子集,尤其是在报告方面(请参阅 显示名称与技术名称)。

4.4.1. 设置

你需要在类路径上具有以下组件及其依赖项。有关组 ID、组件 ID 和版本的详细信息,请参阅 依赖元数据

显式依赖
  • test 作用域中的 junit-platform-runnerJUnitPlatform runner 的位置

  • test 作用域中的 junit-4.13.2.jar:使用 JUnit 4 运行测试

  • test 作用域中的 junit-jupiter-api:用于使用 JUnit Jupiter 编写测试的 API,包括 @Test 等。

  • test runtime 作用域中的 junit-jupiter-engine:JUnit Jupiter 的 TestEngine API 的实现

传递依赖
  • test 作用域中的 junit-platform-suite-api

  • test 作用域中的 junit-platform-suite-commons

  • test 作用域中的 junit-platform-launcher

  • test 作用域中的 junit-platform-engine

  • test 作用域中的 junit-platform-commons

  • test 作用域中的 opentest4j

4.4.2. 显示名称与技术名称

要为通过 @RunWith(JUnitPlatform.class) 运行的类定义自定义显示名称,请使用 @SuiteDisplayName 注解该类并提供自定义值。

默认情况下,显示名称将用于测试组件;但是,当使用 JUnitPlatform runner 通过构建工具(例如 Gradle 或 Maven)执行测试时,生成的测试报告通常需要包含测试组件的技术名称 — 例如,完全限定类名 — 而不是较短的显示名称,例如测试类的简单名称或包含特殊字符的自定义显示名称。要为报告目的启用技术名称,请在 @RunWith(JUnitPlatform.class) 旁边声明 @UseTechnicalNames 注解。

请注意,@UseTechnicalNames 的存在会覆盖通过 @SuiteDisplayName 配置的任何自定义显示名称。

4.4.3. 单个测试类

使用 JUnitPlatform runner 的一种方法是直接使用 @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 开头或以 TestTests 结尾的测试类。

其他配置选项
除了 @SelectPackages 之外,还有更多用于发现和过滤测试的配置选项。有关更多详细信息,请参阅 org.junit.platform.suite.api 包的 Javadoc。
使用 @RunWith(JUnitPlatform.class) 注解的测试类和套件不能直接在 JUnit Platform 上执行(或作为某些 IDE 中记录的“JUnit 5”测试执行)。此类和套件只能使用 JUnit 4 基础设施执行。

4.5. 发现选择器

JUnit Platform 提供了一组丰富的发现选择器,可用于指定应发现或执行哪些测试。

发现选择器可以使用 DiscoverySelectors 类中的工厂方法以编程方式创建,在使用 JUnit Platform Suite Engine 时通过注解声明性地指定,通过 Console Launcher 的选项指定,或通过其标识符以字符串形式泛型地指定。

开箱即用地提供了以下发现选择器

Java 类型 API 注解 控制台启动器 标识符

ClasspathResourceSelector

selectClasspathResource

@SelectClasspathResource

--select-resource /foo.csv

resource:/foo.csv

ClasspathRootSelector

selectClasspathRoots

 — 

--scan-classpath bin

classpath-root:bin

ClassSelector

selectClass

@SelectClasses

--select-class com.acme.Foo

class:com.acme.Foo

DirectorySelector

selectDirectory

@SelectDirectories

--select-directory foo/bar

directory:foo/bar

FileSelector

selectFile

@SelectFile

--select-file dir/foo.txt

file:dir/foo.txt

IterationSelector

selectIteration

@Select("<identifier>")

--select-iteration method=com.acme.Foo#m[1..2]

iteration:method:com.acme.Foo#m[1..2]

MethodSelector

selectMethod

@SelectMethod

--select-method com.acme.Foo#m

method:com.acme.Foo#m

ModuleSelector

selectModule

@SelectModules

--select-module com.acme

module:com.acme

NestedClassSelector

selectNestedClass

@Select("<identifier>")

--select <identifier>

nested-class:com.acme.Foo/Bar

NestedMethodSelector

selectNestedMethod

@Select("<identifier>")

--select <identifier>

nested-method:com.acme.Foo/Bar#m

PackageSelector

selectPackage

@SelectPackages

--select-package com.acme.foo

package:com.acme.foo

UniqueIdSelector

selectUniqueId

@Select("<identifier>")

--select <identifier>

uid:…​

UriSelector

selectUri

@SelectUris

--select-uri file:///foo.txt

uri:file:///foo.txt

4.6. 配置参数

除了指示平台要包含哪些测试类和测试引擎、要扫描哪些包等之外,有时还需要提供特定于特定测试引擎、监听器或已注册扩展的其他自定义配置参数。例如,JUnit Jupiter TestEngine 支持以下用例的配置参数

配置参数是基于文本的键值对,可以通过以下机制之一提供给在 JUnit Platform 上运行的测试引擎。

  1. LauncherDiscoveryRequestBuilder 中的 configurationParameter()configurationParameters() 方法,用于构建提供给 Launcher API 的请求。
    当通过 JUnit Platform 提供的工具之一运行测试时,你可以按如下方式指定配置参数

  2. LauncherDiscoveryRequestBuilder 中的 configurationParametersResources() 方法。
    当通过 Console Launcher 运行测试时,你可以使用 --config-resource 命令行选项指定自定义配置文件。

  3. JVM 系统属性。

  4. JUnit Platform 默认配置文件:类路径根目录中名为 junit-platform.properties 的文件,该文件遵循 Java Properties 文件的语法规则。

配置参数按照上面定义的精确顺序查找。因此,直接提供给 Launcher 的配置参数优先于通过自定义配置文件、系统属性和默认配置文件提供的配置参数。同样,通过系统属性提供的配置参数优先于通过默认配置文件提供的配置参数。

4.6.1. 模式匹配语法

本节介绍应用于以下功能使用的配置参数的模式匹配语法。

如果给定配置参数的值仅由星号 (*) 组成,则该模式将与所有候选类匹配。否则,该值将被视为逗号分隔的模式列表,其中每个模式将与每个候选类的完全限定类名 (FQCN) 匹配。模式中的任何点 (.) 将与 FQCN 中的点 (.) 或美元符号 ($) 匹配。任何星号 (*) 将与 FQCN 中的一个或多个字符匹配。模式中的所有其他字符将与 FQCN 一对一匹配。

示例

  • *:匹配所有候选类。

  • org.junit.*:匹配 org.junit 基础包及其任何子包下的所有候选类。

  • *.MyCustomImpl:匹配每个简单类名完全为 MyCustomImpl 的候选类。

  • *System*:匹配每个 FQCN 包含 System 的候选类。

  • *System*, *Unit*:匹配每个 FQCN 包含 SystemUnit 的候选类。

  • org.example.MyCustomImpl:匹配 FQCN 完全为 org.example.MyCustomImpl 的候选类。

  • org.example.MyCustomImpl, org.example.TheirCustomImpl:匹配 FQCN 完全为 org.example.MyCustomImplorg.example.TheirCustomImpl 的候选类。

4.7. 标签

标签是 JUnit Platform 的一个概念,用于标记和过滤测试。向容器和测试添加标签的编程模型由测试框架定义。例如,在基于 JUnit Jupiter 的测试中,应该使用 @Tag 注解(参见 标签和过滤)。对于基于 JUnit 4 的测试,Vintage 引擎将 @Category 注解映射到标签(参见 Categories Support)。其他测试框架可能会定义他们自己的注解或其他方式供用户指定标签。

4.7.1. 标签的语法规则

无论标签是如何指定的,JUnit Platform 都会强制执行以下规则

  • 标签不能为空(null)或空白

  • 修剪后的标签不得包含空格。

  • 修剪后的标签不得包含 ISO 控制字符。

  • 修剪后的标签不得包含以下任何保留字符

    • ,: 逗号

    • (: 左括号

    • ): 右括号

    • &: 与号

    • |: 竖线

    • !: 感叹号

在上述上下文中,“修剪后”表示已删除前导和尾随空格字符。

4.7.2. 标签表达式

标签表达式是布尔表达式,运算符包括 !&|。此外,可以使用 () 来调整运算符优先级。

支持两个特殊表达式,any()none(),它们分别选择所有带有任何标签的测试和所有不带任何标签的测试。这些特殊表达式可以像普通标签一样与其他表达式组合使用。

表 2. 运算符(按优先级降序排列)
运算符 含义 结合性

!

&

|

如果您在多个维度上标记测试,标签表达式可以帮助您选择要执行的测试。当按测试类型(例如,微服务集成端到端)和功能(例如,产品目录发货)进行标记时,以下标签表达式可能会很有用。

标签表达式 选择

product

所有针对 product(产品) 的测试

catalog | shipping

所有针对 catalog(目录) 的测试加上所有针对 shipping(发货) 的测试

catalog & shipping

所有针对 catalog(目录) 和 shipping(发货) 之间交集的测试

product & !end-to-end

所有针对 product(产品) 的测试,但不包括 端到端 测试

(micro | integration) & (product | shipping)

所有针对 product(产品) 或 shipping(发货) 的 微服务集成 测试

4.8. 捕获标准输出/错误

自 1.3 版本起,JUnit Platform 提供了选择性启用功能,用于捕获打印到 System.outSystem.err 的输出。要启用它,请将 junit.platform.output.capture.stdout 和/或 junit.platform.output.capture.stderr 配置参数设置为 true。此外,您可以使用 junit.platform.output.capture.maxBuffer 配置每个执行的测试或容器要使用的最大缓冲字节数。

如果启用,JUnit Platform 将捕获相应的输出,并立即在报告测试或容器完成之前,使用 stdoutstderr 键将其作为报告条目发布到所有已注册的 TestExecutionListener 实例。

请注意,捕获的输出将仅包含用于执行容器或测试的线程发出的输出。其他线程的任何输出都将被省略,因为特别是在并行执行测试时,将无法将其归因于特定的测试或容器。

4.9. 使用监听器和拦截器

JUnit Platform 提供了以下监听器 API,允许 JUnit、第三方和自定义用户代码对 TestPlan 的发现和执行过程中各个点触发的事件做出反应。

LauncherSessionListener API 通常由构建工具或 IDE 实现,并为您自动注册,以支持构建工具或 IDE 的某些功能。

LauncherDiscoveryListenerTestExecutionListener API 通常被实现以生成某种形式的报告或在 IDE 中显示测试计划的图形表示。此类监听器可以由构建工具或 IDE 实现并自动注册,或者它们可以包含在第三方库中 - 可能会为您自动注册。您也可以实现和注册自己的监听器。

有关注册和配置监听器的详细信息,请参阅本指南的以下部分。

JUnit Platform 提供了以下监听器,您可能希望在测试套件中使用它们。

JUnit Platform 报告

LegacyXmlReportGeneratingListener 可以通过 Console Launcher 使用或手动注册,以生成与基于 JUnit 4 的测试报告的事实标准兼容的 XML 报告。

OpenTestReportGeneratingListenerOpen Test Reporting 指定的基于事件的格式生成 XML 报告。它已自动注册,可以通过 配置参数 启用和配置。

有关详细信息,请参阅 JUnit Platform 报告

Flight Recorder 支持

FlightRecordingExecutionListenerFlightRecordingDiscoveryListener 在测试发现和执行期间生成 Java Flight Recorder 事件。

LoggingListener

TestExecutionListener 用于通过 BiConsumer 记录所有事件的信息性消息,该 BiConsumer 消费 ThrowableSupplier<String>

SummaryGeneratingListener

TestExecutionListener 生成测试执行的摘要,可以通过 PrintWriter 打印。

UniqueIdTrackingListener

TestExecutionListener 跟踪在 TestPlan 执行期间跳过或执行的所有测试的唯一 ID,并在 TestPlan 执行完成后生成包含唯一 ID 的文件。

4.9.1. Flight Recorder 支持

自 1.7 版本起,JUnit Platform 提供了选择性启用功能,用于生成 Flight Recorder 事件。JEP 328 将 Java Flight Recorder (JFR) 描述为

Flight Recorder 记录源自应用程序、JVM 和 OS 的事件。事件存储在单个文件中,该文件可以附加到错误报告并由支持工程师检查,从而可以对问题发生前一段时间内的问题进行事后分析。

为了记录在运行测试时生成的 Flight Recorder 事件,您需要

  1. 确保您使用的是 Java 8 Update 262 或更高版本,或者 Java 11 或更高版本。

  2. 在测试运行时,在类路径或模块路径上提供 org.junit.platform.jfr 模块 (junit-platform-jfr-1.12.0.jar)。

  3. 在启动测试运行时启动 Flight Recorder 记录。Flight Recorder 可以通过 java 命令行选项启动

    -XX:StartFlightRecording:filename=...

请查阅您的构建工具手册以获取相应的命令。

要分析记录的事件,请使用最新 JDK 附带的 jfr 命令行工具,或使用 JDK Mission Control 打开记录文件。

Flight Recorder 支持目前是一项实验性功能。我们邀请您试用并向 JUnit 团队提供反馈,以便他们可以改进并最终推广此功能。

4.10. 堆栈跟踪剪枝

自 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: 支持静态字段注入

  • TestInstancePostProcessor: 支持非静态字段注入

  • ParameterResolver: 支持构造函数和方法注入

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.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.junit.platform.commons.support.ModifierSupport;

class RandomNumberExtension
        implements BeforeAllCallback, TestInstancePostProcessor, 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 postProcessTestInstance(Object testInstance, ExtensionContext context) {
        Class<?> testClass = context.getRequiredTestClass();
        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 声明式注册的扩展将被继承。

有关详细信息,请参阅 扩展继承

@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@ExtendWith 注册的扩展将被继承。

有关详细信息,请参阅 扩展继承

@RegisterExtension 字段不能为空(在评估时),但可以是 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 {
        @JvmField
        @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 在方法级别注册的扩展之前注册。

在以下示例中,测试类中的 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 等的支持)之后的扩展注册表中。

过滤自动检测到的扩展

可以使用以下 配置参数 通过包含和排除模式来过滤自动检测到的扩展列表

junit.jupiter.extensions.autodetection.include=<patterns>

自动检测到的扩展的包含模式的逗号分隔列表。

junit.jupiter.extensions.autodetection.exclude=<patterns>

自动检测到的扩展的排除模式的逗号分隔列表。

包含模式在排除模式之前应用。如果同时提供了包含和排除模式,则只有匹配至少一个包含模式且不匹配任何排除模式的扩展才会被自动检测到。

有关模式语法的详细信息,请参阅 模式匹配语法

5.2.4. 扩展继承

注册的扩展在测试类层次结构中以自顶向下的语义继承。同样,在类级别注册的扩展在方法级别继承。这适用于所有扩展,无论它们是如何注册的(声明式或编程式)。

这意味着通过超类上的 @ExtendWith 声明式注册的扩展将在通过子类上的 @ExtendWith 声明式注册的扩展之前注册。

同样,通过超类中字段上的 @RegisterExtension@ExtendWith 编程式注册的扩展将在通过子类中字段上的 @RegisterExtension@ExtendWith 编程式注册的扩展之前注册,除非使用 @Order 来更改该行为(有关详细信息,请参阅 扩展注册顺序)。

对于给定的扩展上下文及其父上下文,特定的扩展实现只能注册一次。因此,任何注册重复扩展实现的尝试都将被忽略。

5.3. 条件测试执行

ExecutionCondition 定义了用于编程式条件测试执行的 Extension API。

对于每个容器(例如,测试类),都会评估 ExecutionCondition,以确定是否应根据提供的 ExtensionContext 执行其包含的所有测试。同样,对于每个测试,都会评估 ExecutionCondition,以确定是否应根据提供的 ExtensionContext 执行给定的测试方法。

当注册了多个 ExecutionCondition 扩展时,只要其中一个条件返回禁用,容器或测试就会被禁用。因此,不能保证会评估某个条件,因为另一个扩展可能已经导致容器或测试被禁用。换句话说,评估的工作方式类似于短路布尔 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 定义了 Extension 的 API,这些 Extension 希望在构造测试实例之前(通过构造函数调用或通过 TestInstanceFactory)被调用。

此扩展提供对 TestInstancePreDestroyCallback 的对称调用,并且与其他扩展结合使用以准备构造函数参数或跟踪测试实例及其生命周期非常有用。

访问测试作用域的 ExtensionContext

您可以覆盖 getTestInstantiationExtensionContextScope(…​) 方法以返回 TEST_METHOD,以便使特定于测试的数据可用于您的扩展实现,或者如果您想在测试方法级别 保留状态

5.5. 测试实例工厂

TestInstanceFactory 定义了 Extension 的 API,这些 Extension 希望创建测试类实例。

常见的用例包括从依赖注入框架获取测试实例或调用静态工厂方法来创建测试类实例。

如果未注册 TestInstanceFactory,则框架将调用测试类的唯一构造函数来实例化它,并可能通过注册的 ParameterResolver 扩展来解析构造函数参数。

实现 TestInstanceFactory 的扩展可以在测试接口、顶级测试类或 @Nested 测试类上注册。

为任何单个类注册多个实现 TestInstanceFactory 的扩展将导致该类、任何子类和任何嵌套类中的所有测试都抛出异常。请注意,在超类或封闭类(即,在 @Nested 测试类的情况下)中注册的任何 TestInstanceFactory 都是继承的。用户有责任确保仅为任何特定测试类注册单个 TestInstanceFactory

访问测试作用域的 ExtensionContext

您可以覆盖 getTestInstantiationExtensionContextScope(…​) 方法以返回 TEST_METHOD,以便使特定于测试的数据可用于您的扩展实现,或者如果您想在测试方法级别 保留状态

5.6. 测试实例后处理

TestInstancePostProcessor 定义了 Extension 的 API,这些 Extension 希望后处理测试实例。

常见的用例包括将依赖项注入到测试实例中,调用测试实例上的自定义初始化方法等。

有关具体示例,请查阅 MockitoExtensionSpringExtension 的源代码。

访问测试作用域的 ExtensionContext

您可以覆盖 getTestInstantiationExtensionContextScope(…​) 方法以返回 TEST_METHOD,以便使特定于测试的数据可用于您的扩展实现,或者如果您想在测试方法级别 保留状态

5.7. 测试实例预销毁回调

TestInstancePreDestroyCallback 定义了 Extension 的 API,这些 Extension 希望在测试实例已在测试中使用之后并在销毁之前对其进行处理。

常见的用例包括清理已注入到测试实例中的依赖项,调用测试实例上的自定义反初始化方法等。

5.8. 参数解析

ParameterResolver 定义了用于在运行时动态解析参数的 Extension API。

如果测试类构造函数、测试方法生命周期方法(请参阅 定义)声明了一个参数,则该参数必须在运行时由 ParameterResolver 解析。ParameterResolver 可以是内置的(请参阅 TestInfoParameterResolver)或 由用户注册。一般来说,参数可以通过名称类型注解或它们的任意组合来解析。

如果您希望实现一个自定义 ParameterResolver,该 ParameterResolver 仅基于参数类型解析参数,您可能会发现扩展 TypeBasedParameterResolver 很方便,它可以用作此类用例的通用适配器。

例如,要查看具体的示例,请查阅 CustomTypeParameterResolverCustomAnnotationParameterResolverMapOfListsTypeBasedParameterResolver 的源代码。

由于 JDK 9 之前版本中的 javac 生成的字节码存在一个错误,因此对于内部类构造函数(例如,@Nested 测试类中的构造函数),通过核心 java.lang.reflect.Parameter API 直接查找参数上的注解总是会失败。

因此,提供给 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)

访问测试作用域的 ExtensionContext

您可以重写 getTestInstantiationExtensionContextScope(…​) 方法以返回 TEST_METHOD,从而支持将特定于测试的数据注入到测试类实例的构造函数参数中。这样做会导致在解析构造函数参数时使用特定于测试的 ExtensionContext,除非测试实例生命周期设置为 PER_CLASS

从扩展调用的方法进行参数解析

其他扩展也可以利用已注册的 ParameterResolver 进行方法和构造函数调用,方法是使用通过 ExtensionContext 中的 getExecutableInvoker() 方法提供的 ExecutableInvoker

5.8.1. 参数冲突

如果为某个测试注册了多个支持相同类型的 ParameterResolver 实现,则会抛出 ParameterResolutionException,并显示一条消息,指示已发现竞争的解析器。请参见以下示例

由于多个解析器声称支持整数而导致的参数解析冲突
public class ParameterResolverConflictDemo {

    @Test
    @ExtendWith({ FirstIntegerResolver.class, SecondIntegerResolver.class })
    void testInt(int i) {
        // Test will not run due to ParameterResolutionException
        assertEquals(1, i);
    }

    static class FirstIntegerResolver 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 1;
        }
    }

    static class SecondIntegerResolver 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;
        }
    }
}

如果冲突的 ParameterResolver 实现应用于不同的测试方法,如下例所示,则不会发生冲突。

避免冲突的细粒度注册
public class ParameterResolverNoConflictDemo {

    @Test
    @ExtendWith(FirstIntegerResolver.class)
    void firstResolution(int i) {
        assertEquals(1, i);
    }

    @Test
    @ExtendWith(SecondIntegerResolver.class)
    void secondResolution(int i) {
        assertEquals(2, i);
    }

    static class FirstIntegerResolver 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 1;
        }
    }

    static class SecondIntegerResolver 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;
        }
    }
}

如果需要将冲突的 ParameterResolver 实现应用于相同的测试方法,您可以实现自定义类型或自定义注解,如 CustomTypeParameterResolverCustomAnnotationParameterResolver 分别所示。

用于解析重复类型的自定义类型
public class ParameterResolverCustomTypeDemo {

    @Test
    @ExtendWith({ FirstIntegerResolver.class, SecondIntegerResolver.class })
    void testInt(Integer i, WrappedInteger wrappedInteger) {
        assertEquals(1, i);
        assertEquals(2, wrappedInteger.value);
    }

    static class FirstIntegerResolver implements ParameterResolver {

        @Override
        public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
            return parameterContext.getParameter().getType().equals(Integer.class);
        }

        @Override
        public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
            return 1;
        }
    }

    static class SecondIntegerResolver implements ParameterResolver {

        @Override
        public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
            return parameterContext.getParameter().getType().equals(WrappedInteger.class);
        }

        @Override
        public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
            return new WrappedInteger(2);
        }
    }

    static class WrappedInteger {

        private final int value;

        public WrappedInteger(int value) {
            this.value = value;
        }

    }
}

自定义注解使重复类型与其对应类型区分开来

用于解析重复类型的自定义注解
public class ParameterResolverCustomAnnotationDemo {

    @Test
    void testInt(@FirstInteger Integer first, @SecondInteger Integer second) {
        assertEquals(1, first);
        assertEquals(2, second);
    }

    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    @ExtendWith(FirstInteger.Extension.class)
    public @interface FirstInteger {

        class Extension implements ParameterResolver {

            @Override
            public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
                return parameterContext.getParameter().getType().equals(Integer.class)
                        && !parameterContext.isAnnotated(SecondInteger.class);
            }

            @Override
            public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
                return 1;
            }
        }
    }

    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    @ExtendWith(SecondInteger.Extension.class)
    public @interface SecondInteger {

        class Extension implements ParameterResolver {

            @Override
            public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
                return parameterContext.isAnnotated(SecondInteger.class);
            }

            @Override
            public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
                return 2;
            }
        }
    }
}

JUnit 包含一些内置的参数解析器,如果解析器尝试声明其支持的类型,可能会导致冲突。例如,TestInfo 提供有关测试的元数据。有关详细信息,请参阅构造函数和方法的依赖注入。诸如 Spring 之类的第三方框架也可能定义参数解析器。应用本节中的一种技术来解决任何冲突。

参数化测试是另一个潜在的冲突来源。确保使用 @ParameterizedTest 注解的测试没有同时使用 @Test 注解,并参阅使用参数以获取更多详细信息。

5.9. 测试结果处理

TestWatcher 定义了扩展的 API,这些扩展希望处理测试方法执行的结果。具体来说,对于以下事件,将使用上下文信息调用 TestWatcher

  • testDisabled:在禁用的测试方法被跳过后调用

  • testSuccessful:在测试方法成功完成后调用

  • testAborted:在测试方法被中止后调用

  • testFailed:在测试方法失败后调用

定义中提出的“测试方法”的定义相反,在此上下文中,测试方法指的是任何 @Test 方法或 @TestTemplate 方法(例如,@RepeatedTest@ParameterizedTest)。

实现此接口的扩展可以在类级别、实例级别或方法级别注册。在类级别注册时,对于任何包含的测试方法(包括 @Nested 类中的方法),都会调用 TestWatcher。在方法级别注册时,TestWatcher 仅针对注册它的测试方法调用。

如果通过非静态(实例)字段注册 TestWatcher,例如使用 @RegisterExtension,并且测试类配置了 @TestInstance(Lifecycle.PER_METHOD) 语义(这是默认的生命周期模式),则 TestWatcher不会使用 @TestTemplate 方法(例如,@RepeatedTest@ParameterizedTest)的事件调用。

为了确保在给定类中的所有测试方法都调用 TestWatcher,因此建议使用 @ExtendWith 在类级别注册 TestWatcher,或者通过带有 @RegisterExtension@ExtendWithstatic 字段注册。

如果在类级别出现故障(例如,@BeforeAll 方法抛出异常),则不会报告任何测试结果。同样,如果通过 ExecutionCondition 禁用测试类(例如,@Disabled),则不会报告任何测试结果。

与其他 Extension API 相比,不允许 TestWatcher 对测试的执行产生不利影响。因此,TestWatcher API 中方法抛出的任何异常都将在 WARNING 级别记录,并且不允许传播或导致测试执行失败。

存储在提供的 ExtensionContextStore 中的任何 ExtensionContext.Store.CloseableResource 实例都将在调用 TestWatcher API 中的方法之前关闭(请参阅在扩展中保持状态)。您可以使用父上下文的 Store 来处理此类资源。

5.10. 测试生命周期回调

以下接口定义了在测试执行生命周期的各个点扩展测试的 API。有关示例以及 org.junit.jupiter.api.extension 包中每个接口的 Javadoc,请查阅以下各节以获取更多详细信息。

实现多个扩展 API
扩展开发人员可以选择在单个扩展中实现任意数量的这些接口。有关具体示例,请查阅 SpringExtension 的源代码。

5.10.1. 测试执行前后回调

BeforeTestExecutionCallbackAfterTestExecutionCallback 定义了 Extensions 的 API,这些扩展希望添加在测试方法执行之前之后立即执行的行为。因此,这些回调非常适合计时、跟踪和类似用例。如果您需要实现围绕 @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 为希望通过 TestExecutionExceptionHandler 处理 @Test 方法期间抛出的异常的 Extensions,以及为那些通过 LifecycleMethodExecutionExceptionHandler 处理测试生命周期方法(@BeforeAll@BeforeEach@AfterEach@AfterAll)期间抛出的异常的 Extensions 提供了 API。

以下示例展示了一个扩展,该扩展将吞噬所有 IOException 实例,但会重新抛出任何其他类型的异常。

一个在测试执行中过滤 IOExceptions 的异常处理扩展
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 引擎,就好像从未抛出异常一样。处理程序也可以选择重新抛出异常或抛出不同的异常,可能会包装原始异常。

希望处理 @BeforeAll@AfterAll 期间抛出的异常的 LifecycleMethodExecutionExceptionHandler 的实现需要在类级别注册,而 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. 预中断回调

PreInterruptCallback 定义了 Extensions 的 API,这些扩展希望在调用 Thread.interrupt() 之前对超时做出反应。

有关更多信息,请参阅调试超时

5.13. 拦截调用

InvocationInterceptor 定义了 Extensions 的 API,这些扩展希望拦截对测试代码的调用。

以下示例展示了一个扩展,该扩展在 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;
        }
    }
}
访问测试作用域的 ExtensionContext

您可以重写 getTestInstantiationExtensionContextScope(…​) 方法以返回 TEST_METHOD,以便使特定于测试的数据可用于 interceptTestClassConstructor 的扩展实现,或者如果您想在测试方法级别保持状态

5.14. 为测试模板提供调用上下文

只有在至少注册了一个 TestTemplateInvocationContextProvider 时,才能执行 @TestTemplate 方法。每个此类提供程序都负责提供 TestTemplateInvocationContext 实例的 Stream。每个上下文都可以指定自定义显示名称和额外的扩展列表,这些扩展将仅用于下一次 @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.15. 在扩展中保持状态

通常,扩展只实例化一次。因此,问题变得相关:如何保持从一个扩展调用到下一个调用的状态?ExtensionContext API 正是为此目的提供了一个 Store。扩展可以将值放入存储中以供以后检索。请参阅 TimingExtension,以获取将 Store 与方法级别范围一起使用的示例。重要的是要记住,在测试执行期间存储在 ExtensionContext 中的值在周围的 ExtensionContext 中将不可用。由于 ExtensionContexts 可能是嵌套的,因此内部上下文的范围也可能受到限制。有关通过 Store 存储和检索值的可用方法的详细信息,请查阅相应的 Javadoc。

ExtensionContext.Store.CloseableResource
扩展上下文存储绑定到其扩展上下文生命周期。当扩展上下文生命周期结束时,它会关闭其关联的存储。所有存储的值(属于 CloseableResource 实例)都会通过调用其 close() 方法(以添加的相反顺序)收到通知。

下面显示了 CloseableResource 的示例实现,使用 HttpServer 资源。

实现 CloseableResourceHttpServer 资源
class HttpServerResource implements CloseableResource {

    private final HttpServer httpServer;

    HttpServerResource(int port) throws IOException {
        InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
        this.httpServer = HttpServer.create(new InetSocketAddress(loopbackAddress, port), 0);
    }

    HttpServer getHttpServer() {
        return httpServer;
    }

    void start() {
        // Example handler
        httpServer.createContext("/example", exchange -> {
            String body = "This is a test";
            exchange.sendResponseHeaders(200, body.length());
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(body.getBytes(UTF_8));
            }
        });
        httpServer.setExecutor(null);
        httpServer.start();
    }

    @Override
    public void close() {
        httpServer.stop(0);
    }
}

然后,可以将此资源存储在所需的 ExtensionContext 中。如果需要,可以将其存储在类或方法级别,但这可能会为此类资源增加不必要的开销。对于此示例,明智的做法可能是将其存储在根级别并延迟实例化,以确保每个测试运行仅创建一次,并在不同的测试类和方法之间重用。

使用 Store.getOrComputeIfAbsent 在根上下文中延迟存储
public class HttpServerExtension implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return HttpServer.class.equals(parameterContext.getParameter().getType());
    }

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

        ExtensionContext rootContext = extensionContext.getRoot();
        ExtensionContext.Store store = rootContext.getStore(Namespace.GLOBAL);
        String key = HttpServerResource.class.getName();
        HttpServerResource resource = store.getOrComputeIfAbsent(key, __ -> {
            try {
                HttpServerResource serverResource = new HttpServerResource(0);
                serverResource.start();
                return serverResource;
            }
            catch (IOException e) {
                throw new UncheckedIOException("Failed to create HttpServerResource", e);
            }
        }, HttpServerResource.class);
        return resource.getHttpServer();
    }
}
使用 HttpServerExtension 的测试用例
@ExtendWith(HttpServerExtension.class)
public class HttpServerDemo {

    @Test
    void httpCall(HttpServer server) throws Exception {
        String hostName = server.getAddress().getHostName();
        int port = server.getAddress().getPort();
        String rawUrl = String.format("http://%s:%d/example", hostName, port);
        URL requestUrl = URI.create(rawUrl).toURL();

        String responseBody = sendRequest(requestUrl);

        assertEquals("This is a test", responseBody);
    }

    private static String sendRequest(URL url) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        int contentLength = connection.getContentLength();
        try (InputStream response = url.openStream()) {
            byte[] content = new byte[contentLength];
            assertEquals(contentLength, response.read(content));
            return new String(content, UTF_8);
        }
    }
}

5.16. 扩展中支持的实用程序

junit-platform-commons 工件提供了用于处理注解、类、反射、类路径扫描和转换任务的维护实用程序。这些实用程序可以在 org.junit.platform.commons.support 及其子包中找到。建议 TestEngineExtension 作者使用这些受支持的实用程序,以便与 JUnit Platform 和 JUnit Jupiter 的行为保持一致。

5.16.1. 注解支持

AnnotationSupport 提供了静态实用程序方法,这些方法对注解元素(例如,包、注解、类、接口、构造函数、方法和字段)进行操作。这些方法包括检查元素是否使用特定注解或元注解进行注解、搜索特定注解以及查找类或接口中的注解方法和字段的方法。其中一些方法在已实现的接口和类层次结构中搜索以查找注解。有关更多详细信息,请查阅 AnnotationSupport 的 Javadoc。

isAnnotated() 方法不会查找可重复注解。要检查可重复注解,请使用 findRepeatableAnnotations() 方法之一,并验证返回的列表是否不为空。

5.16.2. 类支持

ClassSupport 提供了用于处理类(即 java.lang.Class 的实例)的静态实用程序方法。有关更多详细信息,请查阅 ClassSupport 的 Javadoc。

5.16.3. 反射支持

ReflectionSupport 提供了静态实用程序方法,这些方法增强了标准的 JDK 反射和类加载机制。这些方法包括扫描类路径以搜索与指定谓词匹配的类、加载和创建类的新实例以及查找和调用方法的方法。其中一些方法遍历类层次结构以查找匹配的方法。有关更多详细信息,请查阅 ReflectionSupport 的 Javadoc。

5.16.4. 修饰符支持

ModifierSupport 提供了用于处理成员和类修饰符的静态实用程序方法——例如,确定成员是否声明为 publicprivateabstractstatic 等。有关更多详细信息,请查阅 ModifierSupport 的 Javadoc。

5.16.5. 转换支持

ConversionSupport(在 org.junit.platform.commons.support.conversion 包中)提供了从字符串转换为原始类型及其相应的包装器类型、来自 java.time 包的日期和时间类型以及一些其他常见的 Java 类型(例如 FileBigDecimalBigIntegerCurrencyLocaleURIURLUUID 等)的支持。有关更多详细信息,请查阅 ConversionSupport 的 Javadoc。

5.16.6. 字段和方法搜索语义

AnnotationSupportReflectionSupport 中的各种方法使用搜索算法,这些算法遍历类型层次结构以查找匹配的字段和方法——例如,AnnotationSupport.findAnnotatedFields(…​)ReflectionSupport.findMethods(…​) 等。

从 JUnit 5.11(JUnit Platform 1.11)开始,字段和方法搜索算法遵循标准的 Java 语义,关于根据 Java 语言的规则,给定的字段或方法是否可见或被重写。

在 JUnit 5.11 之前,字段和方法搜索算法应用了我们现在称为“旧版语义”的内容。旧版语义认为字段和方法会被超类型(超类或接口)中的字段和方法隐藏遮蔽取代,这仅仅基于字段的名称或方法的签名,而忽略了关于可见性的实际 Java 语言语义以及确定一个方法是否覆盖另一个方法的规则。

尽管 JUnit 团队建议使用标准搜索语义,但开发人员可以选择通过 junit.platform.reflection.search.useLegacySemantics JVM 系统属性恢复为旧版语义。

例如,要为字段和方法启用旧版搜索语义,您可以使用以下系统属性启动 JVM。

-Djunit.platform.reflection.search.useLegacySemantics=true

由于该功能的底层性质,junit.platform.reflection.search.useLegacySemantics 标志只能通过 JVM 系统属性设置。它不能通过配置参数设置。

5.17. 用户代码和扩展的相对执行顺序

当执行包含一个或多个测试方法的测试类时,除了用户提供的测试和生命周期方法之外,还会调用许多扩展回调。

另请参阅:测试执行顺序

5.17.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.17.2. 回调的包装行为

JUnit Jupiter 始终保证对于实现生命周期回调(例如 BeforeAllCallbackAfterAllCallbackBeforeEachCallbackAfterEachCallbackBeforeTestExecutionCallbackAfterTestExecutionCallback)的多个已注册扩展的包装行为。

这意味着,给定两个扩展 Extension1Extension2,其中 Extension1Extension2 之前注册,由 Extension1 实现的任何“before”回调都保证在由 Extension2 实现的任何“before”回调之前执行。类似地,给定以相同顺序注册的两个相同的扩展,由 Extension1 实现的任何“after”回调都保证在由 Extension2 实现的任何“after”回调之后执行。因此,Extension1 被称为包装 Extension2

JUnit Jupiter 还保证在类和接口层次结构中,对于用户提供的生命周期方法(请参阅定义)的包装行为。

  • @BeforeAll 方法从超类继承,只要它们没有被覆盖。此外,超类中的 @BeforeAll 方法将在子类中的 @BeforeAll 方法之前执行。

    • 类似地,在接口中声明的 @BeforeAll 方法会被继承,只要它们没有被覆盖,并且来自接口的 @BeforeAll 方法将在实现该接口的类中的 @BeforeAll 方法之前执行。

  • @AfterAll 方法从超类继承,只要它们没有被覆盖。此外,超类中的 @AfterAll 方法将在子类中的 @AfterAll 方法之后执行。

    • 类似地,在接口中声明的 @AfterAll 方法会被继承,只要它们没有被覆盖,并且来自接口的 @AfterAll 方法将在实现该接口的类中的 @AfterAll 方法之后执行。

  • @BeforeEach 方法从超类继承,只要它们没有被覆盖。此外,超类中的 @BeforeEach 方法将在子类中的 @BeforeEach 方法之前执行。

    • 类似地,声明为接口默认方法的 @BeforeEach 方法会被继承,只要它们没有被覆盖,并且来自接口的 @BeforeEach 默认方法将在实现该接口的类中的 @BeforeEach 方法之前执行。

  • @AfterEach 方法从超类继承,只要它们没有被覆盖。此外,超类中的 @AfterEach 方法将在子类中的 @AfterEach 方法之后执行。

    • 类似地,声明为接口默认方法的 @AfterEach 方法会被继承,只要它们没有被覆盖,并且来自接口的 @AfterEach 默认方法将在实现该接口的类中的 @AfterEach 方法之后执行。

以下示例演示了此行为。请注意,这些示例实际上并没有做任何实际的事情。相反,它们模拟了与数据库交互的常见场景。所有从 Logger 类静态导入的方法都会记录上下文信息,以便帮助我们更好地理解用户提供的回调方法和扩展中的回调方法的执行顺序。

Extension1
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);
    }

}
Extension2
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);
    }

}
AbstractDatabaseTests
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()");
    }

}
DatabaseTestsDemo
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
DatabaseTestsDemo

JUnit Jupiter 保证在单个测试类或测试接口中声明的多个生命周期方法的执行顺序。有时,JUnit Jupiter 似乎按字母顺序调用这些方法。然而,这并非完全正确。此排序类似于单个测试类中 @Test 方法的排序。

单个测试类或测试接口中声明的生命周期方法将使用确定性但有意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行生命周期方法,从而允许可重复的构建。

此外,JUnit Jupiter 支持为在单个测试类或测试接口中声明的多个生命周期方法提供包装行为。

以下示例演示了此行为。具体来说,生命周期方法配置由于本地声明的生命周期方法的执行顺序而被破坏

  • 测试数据在数据库连接打开之前插入,这导致连接数据库失败。

  • 数据库连接在删除测试数据之前关闭,这导致连接数据库失败。

BrokenLifecycleMethodConfigDemo
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
BrokenLifecycleMethodConfigDemo

由于上述行为,JUnit 团队建议开发人员每个测试类或测试接口最多声明每种类型的生命周期方法(请参阅定义)中的一种,除非这些生命周期方法之间没有依赖关系。

6. 高级主题

6.1. JUnit Platform 报告

junit-platform-reporting 工件包含 TestExecutionListener 实现,这些实现以两种风格生成 XML 测试报告:Open Test Reportinglegacy

该模块还包含其他 TestExecutionListener 实现,可用于构建自定义报告。有关详细信息,请参阅使用监听器和拦截器

6.1.1. 输出目录

JUnit Platform 通过 EngineDiscoveryRequestTestPlan 为注册的 测试引擎监听器 提供 OutputDirectoryProvider。其根目录可以通过以下配置参数进行配置

junit.platform.reporting.output.dir=<路径>

配置报告的输出目录。默认情况下,如果找到 Gradle 构建脚本,则使用 build,如果找到 Maven POM,则使用 target;否则,使用当前工作目录。

要为每次测试运行创建一个唯一的输出目录,您可以在路径中使用 {uniqueNumber} 占位符。例如,reports/junit-{uniqueNumber} 将创建类似 reports/junit-8803697269315188212 的目录。当使用 Gradle 或 Maven 的并行执行功能(创建多个并发运行的 JVM 分叉)时,这可能很有用。

6.1.2. Open Test Reporting

OpenTestReportGeneratingListenerOpen Test Reporting 指定的基于事件的格式为整个执行过程编写 XML 报告,该格式支持 JUnit Platform 的所有功能,例如分层测试结构、显示名称、标签等。

该监听器是自动注册的,可以通过以下配置参数进行配置

junit.platform.reporting.open.xml.enabled=true|false

启用/禁用写入报告。

如果启用,监听器将在配置的输出目录中创建一个名为 open-test-report.xml 的 XML 报告文件。

如果启用了输出捕获,则写入 System.outSystem.err 的捕获输出也将包含在报告中。

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.12.0")
}
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.12.0")
}
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.12.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.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>
Console Launcher

当使用 Console Launcher 时,您可以通过 --config 设置配置参数来启用 Open Test Reporting 输出

$ java -jar junit-platform-console-standalone-1.12.0.jar <OPTIONS> \
  --config=junit.platform.reporting.open.xml.enabled=true \
  --config=junit.platform.reporting.output.dir=reports

配置参数也可以在自定义属性文件中设置,该文件作为类路径资源通过 --config-resource 选项提供

$ java -jar junit-platform-console-standalone-1.12.0.jar <OPTIONS> \
  --config-resource=configuration.properties

6.1.3. Legacy XML 格式

LegacyXmlReportGeneratingListenerTestPlan 中的每个根生成单独的 XML 报告。请注意,生成的 XML 格式与 Ant 构建系统普及的基于 JUnit 4 的测试报告的事实标准兼容。

LegacyXmlReportGeneratingListener 也被 Console Launcher 使用。

6.2. JUnit Platform Suite Engine

JUnit Platform 支持使用 JUnit Platform 从任何测试引擎声明式地定义和执行测试套件。

6.2.1. 设置

除了 junit-platform-suite-apijunit-platform-suite-engine 工件之外,您还需要类路径上至少一个其他测试引擎及其依赖项。有关组 ID、工件 ID 和版本的详细信息,请参阅依赖项元数据

必需的依赖项
  • test 范围中的 junit-platform-suite-api:包含配置测试套件所需的注解的工件

  • test runtime 范围中的 junit-platform-suite-engine:用于声明性测试套件的 TestEngine API 的实现

这两个必需的依赖项都聚合在 junit-platform-suite 工件中,该工件可以在 test 范围中声明,而不是显式声明对 junit-platform-suite-apijunit-platform-suite-engine 的依赖项。
传递依赖项
  • test 作用域中的 junit-platform-suite-commons

  • test 作用域中的 junit-platform-launcher

  • test 作用域中的 junit-platform-engine

  • test 作用域中的 junit-platform-commons

  • test 作用域中的 opentest4j

6.2.2. @Suite 示例

通过使用 @Suite 注解类,它在 JUnit Platform 上被标记为测试套件。如以下示例所示,然后可以使用选择器和过滤器注解来控制套件的内容。

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.2.3. @BeforeSuite 和 @AfterSuite

@BeforeSuite@AfterSuite 注解可以在 @Suite 注解的类中的方法上使用。它们将在测试套件的所有测试之前和之后分别执行。

@Suite
@SelectPackages("example")
class BeforeAndAfterSuiteDemo {

    @BeforeSuite
    static void beforeSuite() {
        // executes before the test suite
    }

    @AfterSuite
    static void afterSuite() {
        // executes after the test suite
    }

}

6.3. JUnit Platform Test Kit

junit-platform-testkit 工件提供对在 JUnit Platform 上执行测试计划然后验证预期结果的支持。从 JUnit Platform 1.12.0 开始,此支持仅限于单个 TestEngine 的执行(请参阅Engine Test Kit)。

6.3.1. Engine Test Kit

org.junit.platform.testkit.engine 包提供对在 JUnit Platform 上运行的给定 TestEngine 执行 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. 断言统计信息

Test Kit 最常见的功能之一是能够针对 TestPlan 执行期间触发的事件断言统计信息。以下测试演示了如何在 JUnit Jupiter TestEngine 中断言容器测试的统计信息。有关可用统计信息的详细信息,请查阅 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 Jupiter TestEngine
2 选择 ExampleTestCase 测试类。
3 执行 TestPlan
4 容器事件过滤。
5 断言容器事件的统计信息。
6 测试事件过滤。
7 断言测试事件的统计信息。
verifyJupiterContainerStats() 测试方法中,startedsucceeded 统计信息的计数为 2,因为 JupiterTestEngineExampleTestCase 类都被视为容器。

6.3.3. 断言事件

如果您发现仅断言统计信息不足以验证测试执行的预期行为,您可以直接使用记录的 Event 元素并针对它们执行断言。

例如,如果您想验证 ExampleTestCase 中的 skippedTest() 方法被跳过的原因,您可以按如下方式操作。

以下示例中的 assertThatEvents() 方法是来自 AssertJ 断言库的 org.assertj.core.api.Assertions.assertThat(events.list()) 的快捷方式。

有关可用于针对事件的 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 Jupiter TestEngine
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(it -> it.endsWith("by zero")))));
    }

}
1 选择 JUnit Jupiter TestEngine
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(it -> it.endsWith("by zero")))),
                event(container(ExampleTestCase.class), finishedSuccessfully()),
                event(engine(), finishedSuccessfully()));
    }

}
1 选择 JUnit Jupiter TestEngine
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 的启动基础设施中。

launcher API 在 junit-platform-launcher 模块中。

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 文件中实现的 PostDiscoveryFilter 和声明的 example.CustomTagFilter 类将被自动加载和应用。

6.4.5. 注册 LauncherSessionListener

LauncherSession 打开时(在 Launcher 首次发现和执行测试之前)和关闭时(当不再发现或执行测试时),将通知 LauncherSessionListener 的已注册实现。可以通过传递给 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.launcher.interceptors.enabled 配置参数只能作为 JVM 系统属性或通过 JUnit Platform 配置文件提供(有关详细信息,请参阅配置参数)。此配置参数不能在传递给 LauncherLauncherDiscoveryRequest 中提供。

一个典型的用例是创建一个自定义拦截器来替换 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 文件中实现的 LauncherDiscoveryListener 和声明的 example.CustomLauncherDiscoveryListener 类将被自动加载和注册。

6.4.8. 注册 TestExecutionListener

除了用于以编程方式注册测试执行监听器的公共 Launcher API 方法之外,自定义 TestExecutionListener 实现将在运行时通过 Java 的 ServiceLoader 机制发现,并自动注册到通过 LauncherFactory 创建的 Launcher

例如,在 /META-INF/services/org.junit.platform.launcher.TestExecutionListener 文件中实现的 TestExecutionListener 和声明的 example.CustomTestExecutionListener 类将被自动加载和注册。

6.4.9. 配置 TestExecutionListener

当通过 Launcher API 以编程方式注册 TestExecutionListener 时,监听器可以提供以编程方式配置它的方法 — 例如,通过其构造函数、setter 方法等。但是,当通过 Java 的 ServiceLoader 机制自动注册 TestExecutionListener 时(请参阅注册 TestExecutionListener),用户无法直接配置监听器。在这种情况下,TestExecutionListener 的作者可以选择通过配置参数使监听器可配置。然后,监听器可以通过提供给 testPlanExecutionStarted(TestPlan)testPlanExecutionFinished(TestPlan) 回调方法的 TestPlan 访问配置参数。有关示例,请参阅 UniqueIdTrackingListener

6.4.10. 停用 TestExecutionListener

有时,在激活某些执行监听器的情况下运行测试套件可能很有用。例如,您可能有一个自定义的 TestExecutionListener,它将测试结果发送到外部系统以进行报告,而在调试时,您可能不希望报告这些调试结果。为此,请为 junit.platform.execution.listeners.deactivate 配置参数提供一个模式,以指定应为当前测试运行停用(即不注册)哪些执行监听器。

只有通过 ServiceLoader 机制在 /META-INF/services/org.junit.platform.launcher.TestExecutionListener 文件中注册的监听器才能被停用。换句话说,任何通过 LauncherDiscoveryRequest 显式注册的 TestExecutionListener 都不能通过 junit.platform.execution.listeners.deactivate 配置参数停用。

此外,由于执行监听器在测试运行开始之前注册,因此 junit.platform.execution.listeners.deactivate 配置参数只能作为 JVM 系统属性或通过 JUnit Platform 配置文件提供(有关详细信息,请参阅配置参数)。此配置参数不能在传递给 LauncherLauncherDiscoveryRequest 中提供。

模式匹配语法

有关详细信息,请参阅 模式匹配语法

6.4.11. 配置 Launcher

如果您需要对测试引擎和监听器的自动检测和注册进行细粒度控制,您可以创建 LauncherConfig 的实例,并将其提供给 LauncherFactory。通常,LauncherConfig 的实例是通过内置的流畅 builder 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. Dry-Run 模式

当通过 Launcher API 运行测试时,您可以通过将 junit.platform.execution.dryRun.enabled 配置参数设置为 true 来启用dry-run 模式。在此模式下,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 团队的 TestEngine 保留

JUnit Platform Launcher 强制规定只有 JUnit 团队发布的 TestEngine 实现才能为其 TestEngine ID 使用 junit- 前缀。

  • 如果任何第三方 TestEngine 声称是 junit-jupiterjunit-vintage,则会抛出异常,立即停止 JUnit Platform 的执行。

  • 如果任何第三方 TestEngine 为其 ID 使用 junit- 前缀,则会记录警告消息。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 Test Engines 的实现或 JUnit 5 wiki 中列出的第三方测试引擎的实现。 您还可以在互联网上找到各种教程和博客,演示如何编写自定义 TestEngine

HierarchicalTestEngineTestEngine SPI(由 junit-jupiter-engine 使用)的便捷抽象基类实现,它仅要求实现者提供测试发现的逻辑。 它实现了 TestDescriptors 的执行,这些 TestDescriptors 实现了 Node 接口,包括对并行执行的支持。

6.5.3. 注册 TestEngine

TestEngine 注册通过 Java 的 ServiceLoader 机制支持。

例如,junit-jupiter-engine 模块在其 JAR 中的 /META-INF/services 文件夹中名为 org.junit.platform.engine.TestEngine 的文件中注册了其 org.junit.jupiter.engine.JupiterTestEngine

6.5.4. 要求

本节中的“必须 (must)”,“不得 (must not)”,“必需 (required)”,“应 (shall)”,“不应 (shall not)”,“应该 (should)”,“不应该 (should not)”,“推荐 (recommended)”,“可以 (may)”和“可选 (optional)”等词语应按照 RFC 2119 中的描述进行解释。
强制性要求

为了与构建工具和 IDE 互操作,TestEngine 实现必须遵守以下要求

  • TestEngine.discover() 返回的 TestDescriptor 必须TestDescriptor 实例树的根。 这意味着在节点及其后代之间不得存在任何循环。

  • TestEngine 必须 能够为先前从 TestEngine.discover() 生成和返回的任何唯一 ID 发现 UniqueIdSelectors。 这使得能够选择要执行或重新运行的测试子集。

  • 传递给 TestEngine.execute()EngineExecutionListenerexecutionSkippedexecutionStartedexecutionFinished 方法必须为从 TestEngine.discover() 返回的树中的每个 TestDescriptor 节点调用至多一次。 父节点必须在其子节点之前报告为已启动,并在其子节点之后报告为已完成。 如果节点报告为已跳过,则不得为其后代报告任何事件。

增强的兼容性

遵守以下要求是可选的,但建议为了增强与构建工具和 IDE 的兼容性

  • 除非指示空发现结果,否则从 TestEngine.discover() 返回的 TestDescriptor 应该有子节点,而不是完全动态的。 这允许工具显示测试的结构并选择要执行的测试子集。

  • 当解析 UniqueIdSelectors 时,TestEngine 应该仅返回具有匹配的唯一 ID(包括其祖先)的 TestDescriptor 实例,但可以返回执行所选测试所需的其他兄弟节点或其他节点。

  • TestEngines 应该支持标记测试和容器,以便在发现测试时可以应用标签过滤器。

7. API 演进

JUnit 5 的主要目标之一是提高维护人员演进 JUnit 的能力,尽管它已在许多项目中使用。 在 JUnit 4 中,许多最初作为内部构造添加的内容仅被外部扩展编写者和工具构建者使用。 这使得更改 JUnit 4 特别困难,有时甚至不可能。

这就是 JUnit 5 为所有公开可用的接口、类和方法引入定义的生命周期的原因。

7.1. API 版本和状态

每个发布的工件都有一个版本号 <major>.<minor>.<patch>,并且所有公开可用的接口、类和方法都使用来自 @API Guardian 项目的 @API 进行注释。 注释的 status 属性可以分配以下值之一。

状态 描述

INTERNAL

不得被 JUnit 自身以外的任何代码使用。 可能会在没有事先通知的情况下删除。

DEPRECATED

应不再使用; 可能会在下一个小版本中消失。

EXPERIMENTAL

旨在用于新的实验性功能,我们在其中寻求反馈。
请谨慎使用此元素; 它将来可能会升级为 MAINTAINEDSTABLE,但也可能会在没有事先通知的情况下删除,即使是在补丁版本中。

MAINTAINED

旨在用于在当前主要版本的至少下一个小版本中不会以向后不兼容的方式更改的功能。 如果计划删除,它将首先降级为 DEPRECATED

STABLE

旨在用于在当前主要版本 (5.*) 中不会以向后不兼容的方式更改的功能。

如果 @API 注释存在于类型上,则认为它也适用于该类型的所有公共成员。 成员可以声明稳定性较低的不同 status 值。

7.2. 实验性 API

下表列出了当前通过 @API(status = EXPERIMENTAL) 指定为实验性的 API。 依赖此类 API 时应谨慎。

包名 名称 始于

org.junit.jupiter.api

AssertionsKt.assertInstanceOf(Object, Function0<String>) (方法)

5.11

org.junit.jupiter.api

AssertionsKt.assertInstanceOf(Object, String) (方法)

5.11

org.junit.jupiter.api

AssertionsKt.assertNotNull(Object) (方法)

5.12

org.junit.jupiter.api

AssertionsKt.assertNotNull(Object, Function0<String>) (方法)

5.12

org.junit.jupiter.api

AssertionsKt.assertNotNull(Object, String) (方法)

5.12

org.junit.jupiter.api

AssertionsKt.assertNull(Object) (方法)

5.12

org.junit.jupiter.api

AssertionsKt.assertNull(Object, Function0<String>) (方法)

5.12

org.junit.jupiter.api

AssertionsKt.assertNull(Object, String) (方法)

5.12

org.junit.jupiter.api

AssertionsKt.fail_nonNullableLambda(Function0<String>) (方法)

5.12

org.junit.jupiter.api

AutoClose (注解)

5.11

org.junit.jupiter.api

DisplayNameGenerator.generateDisplayNameForMethod(List<Class<?>>, Class<?>, Method) (方法)

5.12

org.junit.jupiter.api

DisplayNameGenerator.generateDisplayNameForNestedClass(List<Class<?>>, Class<?>) (方法)

5.12

org.junit.jupiter.api

DynamicTest.stream(Iterator<? extends T>) (方法)

5.11

org.junit.jupiter.api

DynamicTest.stream(Stream<? extends T>) (方法)

5.11

org.junit.jupiter.api

NamedExecutable (接口)

5.11

org.junit.jupiter.api

RepeatedTest.failureThreshold() (注解属性)

5.10

org.junit.jupiter.api

RepetitionInfo.getFailureCount() (方法)

5.10

org.junit.jupiter.api

RepetitionInfo.getFailureThreshold() (方法)

5.10

org.junit.jupiter.api

TestReporter.publishDirectory(Path) (方法)

5.12

org.junit.jupiter.api

TestReporter.publishDirectory(String, ThrowingConsumer<Path>) (方法)

5.12

org.junit.jupiter.api

TestReporter.publishFile(Path, MediaType) (方法)

5.12

org.junit.jupiter.api

TestReporter.publishFile(String, MediaType, ThrowingConsumer<Path>) (方法)

5.12

org.junit.jupiter.api.condition

DisabledForJreRange.maxVersion() (注解属性)

5.12

org.junit.jupiter.api.condition

DisabledForJreRange.minVersion() (注解属性)

5.12

org.junit.jupiter.api.condition

DisabledOnJre.versions() (注解属性)

5.12

org.junit.jupiter.api.condition

EnabledForJreRange.maxVersion() (注解属性)

5.12

org.junit.jupiter.api.condition

EnabledForJreRange.minVersion() (注解属性)

5.12

org.junit.jupiter.api.condition

EnabledOnJre.versions() (注解属性)

5.12

org.junit.jupiter.api.condition

JRE.currentVersionNumber() (方法)

5.12

org.junit.jupiter.api.condition

JRE.isCurrentVersion(int) (方法)

5.12

org.junit.jupiter.api.condition

JRE.version() (方法)

5.12

org.junit.jupiter.api.extension

AnnotatedElementContext (接口)

5.10

org.junit.jupiter.api.extension

ExtensionContext.publishDirectory(String, ThrowingConsumer<Path>) (方法)

5.12

org.junit.jupiter.api.extension

ExtensionContext.publishFile(String, MediaType, ThrowingConsumer<Path>) (方法)

5.12

org.junit.jupiter.api.extension

ExtensionContextException.<init>(String, Throwable) (构造函数)

5.10

org.junit.jupiter.api.extension

MediaType (类)

5.12

org.junit.jupiter.api.extension

ParameterContext.getAnnotatedElement() (方法)

5.10

org.junit.jupiter.api.extension

PreInterruptCallback (接口)

5.12

org.junit.jupiter.api.extension

PreInterruptContext (接口)

5.12

org.junit.jupiter.api.extension

TestInstantiationAwareExtension (接口)

5.12

org.junit.jupiter.api.extension

TestInstantiationAwareExtension$ExtensionContextScope (枚举)

5.12

org.junit.jupiter.api.extension

TestTemplateInvocationContextProvider.mayReturnZeroTestTemplateInvocationContexts(ExtensionContext) (方法)

5.12

org.junit.jupiter.api.io

TempDir.factory() (注解属性)

5.10

org.junit.jupiter.api.io

TempDirFactory (接口)

5.10

org.junit.jupiter.api.parallel

ResourceLock.providers() (注解属性)

5.12

org.junit.jupiter.api.parallel

ResourceLock.target() (注解属性)

5.12

org.junit.jupiter.api.parallel

ResourceLockTarget (枚举)

5.12

org.junit.jupiter.api.parallel

ResourceLocksProvider (接口)

5.12

org.junit.jupiter.params

ArgumentCountValidationMode (枚举)

5.12

org.junit.jupiter.params

ParameterizedTest.allowZeroInvocations() (注解属性)

5.12

org.junit.jupiter.params

ParameterizedTest.argumentCountValidation() (注解属性)

5.12

org.junit.jupiter.params.converter

AnnotationBasedArgumentConverter (类)

5.10

org.junit.jupiter.params.converter

JavaTimeConversionPattern.nullable() (注解属性)

5.12

org.junit.jupiter.params.provider

AnnotationBasedArgumentsProvider (类)

5.10

org.junit.jupiter.params.provider

Arguments$ArgumentSet (类)

5.11

org.junit.jupiter.params.provider

Arguments.argumentSet(String, Object[]) (方法)

5.11

org.junit.jupiter.params.provider

EnumSource.from() (注解属性)

5.12

org.junit.jupiter.params.provider

EnumSource.to() (注解属性)

5.12

org.junit.jupiter.params.provider

FieldSource (注解)

5.11

org.junit.jupiter.params.provider

FieldSources (注解)

5.11

org.junit.platform.commons.support

AnnotationSupport.findAnnotation(Class<?>, Class<A>, List<Class<?>>) (方法)

1.12

org.junit.platform.commons.support

ReflectionSupport.findAllResourcesInClasspathRoot(URI, Predicate<Resource>) (方法)

1.11

org.junit.platform.commons.support

ReflectionSupport.findAllResourcesInModule(String, Predicate<Resource>) (方法)

1.11

org.junit.platform.commons.support

ReflectionSupport.findAllResourcesInPackage(String, Predicate<Resource>) (方法)

1.11

org.junit.platform.commons.support

ReflectionSupport.makeAccessible(Field) (方法)

1.12

org.junit.platform.commons.support

ReflectionSupport.streamAllResourcesInClasspathRoot(URI, Predicate<Resource>) (方法)

1.11

org.junit.platform.commons.support

ReflectionSupport.streamAllResourcesInModule(String, Predicate<Resource>) (方法)

1.11

org.junit.platform.commons.support

ReflectionSupport.streamAllResourcesInPackage(String, Predicate<Resource>) (方法)

1.11

org.junit.platform.commons.support

ReflectionSupport.tryToGetResources(String) (方法)

1.12

org.junit.platform.commons.support

ReflectionSupport.tryToGetResources(String, ClassLoader) (方法)

1.12

org.junit.platform.commons.support

ReflectionSupport.tryToLoadClass(String, ClassLoader) (方法)

1.10

org.junit.platform.commons.support

Resource (接口)

1.11

org.junit.platform.commons.support.conversion

ConversionException (类)

1.11

org.junit.platform.commons.support.conversion

ConversionSupport (类)

1.11

org.junit.platform.commons.support.scanning

ClassFilter (类)

1.12

org.junit.platform.commons.support.scanning

ClasspathScanner (接口)

1.12

org.junit.platform.engine

DiscoverySelector.toIdentifier() (方法)

1.11

org.junit.platform.engine

DiscoverySelectorIdentifier (类)

1.11

org.junit.platform.engine

EngineDiscoveryRequest.getOutputDirectoryProvider() (方法)

1.12

org.junit.platform.engine

EngineExecutionListener.fileEntryPublished(TestDescriptor, FileEntry) (方法)

1.12

org.junit.platform.engine

ExecutionRequest.getOutputDirectoryProvider() (方法)

1.12

org.junit.platform.engine

TestDescriptor.orderChildren(UnaryOperator<List<TestDescriptor>>) (方法)

1.12

org.junit.platform.engine.discovery

ClassSelector.getClassLoader() (方法)

1.10

org.junit.platform.engine.discovery

ClasspathResourceSelector.getClasspathResources() (方法)

1.12

org.junit.platform.engine.discovery

DiscoverySelectorIdentifierParser (接口)

1.11

org.junit.platform.engine.discovery

DiscoverySelectors.parse(DiscoverySelectorIdentifier) (方法)

1.11

org.junit.platform.engine.discovery

DiscoverySelectors.parse(String) (方法)

1.11

org.junit.platform.engine.discovery

DiscoverySelectors.parseAll(Collection<DiscoverySelectorIdentifier>) (方法)

1.11

org.junit.platform.engine.discovery

DiscoverySelectors.parseAll(String[]) (方法)

1.11

org.junit.platform.engine.discovery

DiscoverySelectors.selectClass(ClassLoader, String) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectClasspathResource(Set<Resource>) (方法)

1.12

org.junit.platform.engine.discovery

DiscoverySelectors.selectIteration(DiscoverySelector, int[]) (方法)

1.9

org.junit.platform.engine.discovery

DiscoverySelectors.selectMethod(Class<?>, String, Class<?>[]) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectMethod(ClassLoader, String) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectMethod(ClassLoader, String, String) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectMethod(ClassLoader, String, String, String) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectMethod(String, String, Class<?>[]) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectNestedClass(ClassLoader, List<String>, String) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectNestedMethod(ClassLoader, List<String>, String, String) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectNestedMethod(ClassLoader, List<String>, String, String, String) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectNestedMethod(List<Class<?>>, Class<?>, String, Class<?>[]) (方法)

1.10

org.junit.platform.engine.discovery

DiscoverySelectors.selectNestedMethod(List<String>, String, String, Class<?>[]) (方法)

1.10

org.junit.platform.engine.discovery

IterationSelector (类)

1.9

org.junit.platform.engine.discovery

MethodSelector.getClassLoader() (方法)

1.10

org.junit.platform.engine.discovery

MethodSelector.getParameterTypes() (方法)

1.10

org.junit.platform.engine.discovery

NestedClassSelector.getClassLoader() (方法)

1.10

org.junit.platform.engine.discovery

NestedMethodSelector.getClassLoader() (方法)

1.10

org.junit.platform.engine.discovery

NestedMethodSelector.getParameterTypes() (方法)

1.10

org.junit.platform.engine.reporting

FileEntry (类)

1.12

org.junit.platform.engine.reporting

OutputDirectoryProvider (接口)

1.12

org.junit.platform.engine.support.discovery

EngineDiscoveryRequestResolver$Builder.addResourceContainerSelectorResolver(Predicate<Resource>) (方法)

1.12

org.junit.platform.engine.support.discovery

EngineDiscoveryRequestResolver$InitializationContext.getPackageFilter() (方法)

1.12

org.junit.platform.engine.support.discovery

SelectorResolver.resolve(IterationSelector, Context) (方法)

1.9

org.junit.platform.engine.support.store

NamespacedHierarchicalStore (类)

1.10

org.junit.platform.engine.support.store

NamespacedHierarchicalStoreException (类)

1.10

org.junit.platform.launcher

LauncherInterceptor (接口)

1.10

org.junit.platform.launcher

MethodFilter (接口)

1.12

org.junit.platform.launcher

TestExecutionListener.fileEntryPublished(TestIdentifier, FileEntry) (方法)

1.12

org.junit.platform.launcher

TestPlan$Visitor (接口)

1.10

org.junit.platform.launcher

TestPlan.accept(Visitor) (方法)

1.10

org.junit.platform.launcher

TestPlan.getOutputDirectoryProvider() (方法)

1.12

org.junit.platform.launcher.core

LauncherDiscoveryRequestBuilder.outputDirectoryProvider(OutputDirectoryProvider) (方法)

1.12

org.junit.platform.reporting.open.xml

OpenTestReportGeneratingListener (类)

1.9

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api

Appendable (接口)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api

ChildElement (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api

Context (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api

DocumentWriter (接口)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api

Element (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api

Factory (接口)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api

NamespaceRegistry (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Attachments (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

CoreFactory (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

CpuCores (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Data (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

DirectorySource (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

File (类)

0.2.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

FilePosition (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

FileSource (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

HostName (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Infrastructure (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Metadata (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

OperatingSystem (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Output (类)

0.2.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Reason (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Result (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Sources (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Tag (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

Tags (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

UriSource (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.core

UserName (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.git

Branch (类)

0.2.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.git

Commit (类)

0.2.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.git

GitFactory (类)

0.2.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.git

Repository (类)

0.2.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.git

Status (类)

0.2.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

ClassSource (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

ClasspathResourceSource (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

FileEncoding (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

HeapSize (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

JavaFactory (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

JavaVersion (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

MethodSource (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

PackageSource (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.java

Throwable (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.root

Event (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.root

Events (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.root

Finished (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.root

Reported (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.root

RootFactory (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.root

Started (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.schema

Namespace (类)

0.1.0

org.junit.platform.reporting.shadow.org.opentest4j.reporting.schema

QualifiedName (类)

0.1.0

org.junit.platform.suite.api

AfterSuite (注解)

1.11

org.junit.platform.suite.api

BeforeSuite (注解)

1.11

org.junit.platform.suite.api

ConfigurationParametersResource (注解)

1.11

org.junit.platform.suite.api

ConfigurationParametersResources (注解)

1.11

org.junit.platform.suite.api

Select (注解)

1.11

org.junit.platform.suite.api

SelectClasses.names() (注解属性)

1.10

org.junit.platform.suite.api

SelectMethod (注解)

1.10

org.junit.platform.suite.api

SelectMethods (注解)

1.10

org.junit.platform.suite.api

Selects (注解)

1.11

org.junit.platform.testkit.engine

EngineTestKit$Builder.outputDirectoryProvider(OutputDirectoryProvider) (方法)

1.12

org.junit.platform.testkit.engine

Event.fileEntryPublished(TestDescriptor, FileEntry) (方法)

1.12

org.junit.platform.testkit.engine

EventConditions.fileEntry(Predicate<FileEntry>) (方法)

1.12

org.junit.platform.testkit.engine

EventStatistics.fileEntryPublished(long) (方法)

1.12

org.junit.platform.testkit.engine

Events.fileEntryPublished() (方法)

1.12

org.junit.platform.testkit.engine

ExecutionRecorder.fileEntryPublished(TestDescriptor, FileEntry) (方法)

1.12

org.junit.platform.testkit.engine

TestExecutionResultConditions.rootCause(Condition<Throwable>[]) (方法)

1.11

7.3. 废弃的 API

下表列出了当前通过 @API(status = DEPRECATED) 指定为废弃的 API。 您应尽可能避免使用废弃的 API,因为此类 API 可能会在即将发布的版本中删除。

包名 名称 始于

org.junit.jupiter.api

DisplayNameGenerator.generateDisplayNameForMethod(Class<?>, Method) (方法)

5.12

org.junit.jupiter.api

DisplayNameGenerator.generateDisplayNameForNestedClass(Class<?>) (方法)

5.12

org.junit.jupiter.api

MethodOrderer$Alphanumeric (类)

5.7

org.junit.jupiter.api.condition

JRE.currentVersion() (方法)

5.12

org.junit.jupiter.api.extension

InvocationInterceptor.interceptDynamicTest(Invocation<Void>, ExtensionContext) (方法)

5.8

org.junit.platform.commons.support

AnnotationSupport.findAnnotation(Class<?>, Class<A>, SearchOption) (方法)

1.12

org.junit.platform.commons.support

ReflectionSupport.loadClass(String) (方法)

1.4

org.junit.platform.commons.support

SearchOption (枚举)

1.12

org.junit.platform.commons.util

BlacklistedExceptions (类)

1.7

org.junit.platform.commons.util

PreconditionViolationException (类)

1.5

org.junit.platform.commons.util

ReflectionUtils.loadClass(String) (方法)

1.4

org.junit.platform.commons.util

ReflectionUtils.loadClass(String, ClassLoader) (方法)

1.4

org.junit.platform.commons.util

ReflectionUtils.readFieldValue(Class<T>, String, T) (方法)

1.4

org.junit.platform.commons.util

ReflectionUtils.readFieldValue(Field) (方法)

1.4

org.junit.platform.commons.util

ReflectionUtils.readFieldValue(Field, Object) (方法)

1.4

org.junit.platform.engine

ConfigurationParameters.size() (方法)

1.9

org.junit.platform.engine

ExecutionRequest.<init>(TestDescriptor, EngineExecutionListener, ConfigurationParameters) (构造函数)

1.11

org.junit.platform.engine

ExecutionRequest.create(TestDescriptor, EngineExecutionListener, ConfigurationParameters) (方法)

1.11

org.junit.platform.engine.discovery

MethodSelector.getMethodParameterTypes() (方法)

1.10

org.junit.platform.engine.discovery

NestedMethodSelector.getMethodParameterTypes() (方法)

1.10

org.junit.platform.engine.reporting

ReportEntry.<init>() (构造函数)

1.8

org.junit.platform.engine.support.filter

ClasspathScanningSupport (类)

1.5

org.junit.platform.engine.support.hierarchical

SingleTestExecutor (类)

1.2

org.junit.platform.launcher

TestPlan.add(TestIdentifier) (方法)

1.4

org.junit.platform.launcher

TestPlan.getChildren(String) (方法)

1.10

org.junit.platform.launcher

TestPlan.getTestIdentifier(String) (方法)

1.10

org.junit.platform.launcher.core

LauncherDiscoveryRequestBuilder.<init>() (构造函数)

1.8

org.junit.platform.launcher.listeners

LegacyReportingUtils (类)

1.6

org.junit.platform.runner

JUnitPlatform (类)

1.8

org.junit.platform.suite.api

UseTechnicalNames (注解)

1.8

org.junit.platform.testkit.engine

EngineTestKit$Builder.filters(DiscoveryFilter<?>[]) (方法)

1.7

org.junit.platform.testkit.engine

EngineTestKit.execute(String, EngineDiscoveryRequest) (方法)

1.7

org.junit.platform.testkit.engine

EngineTestKit.execute(TestEngine, EngineDiscoveryRequest) (方法)

1.7

7.4. @API 工具支持

@API Guardian 项目计划为使用 @API 注释的 API 的发布者和消费者提供工具支持。 例如,工具支持可能会提供一种方法来检查 JUnit API 的使用是否符合 @API 注释声明。

8. 贡献者

直接在 GitHub 上浏览当前的贡献者列表

9. 发行说明

发行说明可在此处获取:此处

10. 附录

10.1. 可重现构建

从 5.7 版本开始,JUnit 5 旨在使其非 javadoc JAR 成为可重现的

在相同的构建条件下(例如 Java 版本),重复构建应提供相同的逐字节输出。

这意味着任何人都可以重现 Maven Central/Sonatype 上工件的构建条件,并在本地生成相同的输出工件,从而确认存储库中的工件实际上是从此源代码生成的。

10.2. 依赖元数据

最终版本和里程碑版本的工件部署到 Maven Central,快照工件部署到 Sonatype 的 快照存储库,路径为 /org/junit

以下各节列出了平台 (Platform)、木星 (Jupiter) 和古董 (Vintage) 这三个组的所有工件及其版本:平台木星古董物料清单 (BOM) 包含上述所有工件及其版本的列表。

对齐依赖版本

为确保所有 JUnit 工件彼此兼容,它们的版本应保持一致。 如果您依赖 Spring Boot 进行依赖管理,请参阅相应章节。 否则,建议应用 BOM 到您的项目,而不是管理 JUnit 工件的各个版本。 请参阅 MavenGradle 的相应章节。

10.2.1. JUnit 平台

  • 组 ID: org.junit.platform

  • 版本: 1.12.0

  • 工件 ID:

    junit-platform-commons

    JUnit 平台的通用 API 和支持实用程序。 任何使用 @API(status = INTERNAL) 注释的 API 仅供 JUnit 框架本身内部使用。 不支持外部各方使用任何内部 API!

    junit-platform-console

    支持从控制台发现和执行 JUnit 平台上的测试。 有关详细信息,请参阅控制台启动器

    junit-platform-console-standalone

    Maven Central 的 junit-platform-console-standalone 目录中提供了包含所有依赖项的可执行胖 JAR。 有关详细信息,请参阅控制台启动器

    junit-platform-engine

    测试引擎的公共 API。 有关详细信息,请参阅注册 TestEngine

    junit-platform-jfr

    为 JUnit 平台上的 Java Flight Recorder 事件提供 LauncherDiscoveryListenerTestExecutionListener。 有关详细信息,请参阅Flight Recorder 支持

    junit-platform-launcher

    用于配置和启动测试计划的公共 API,通常由 IDE 和构建工具使用。 有关详细信息,请参阅JUnit 平台启动器 API

    junit-platform-reporting

    生成测试报告的 TestExecutionListener 实现,通常由 IDE 和构建工具使用。 有关详细信息,请参阅JUnit 平台报告

    junit-platform-runner

    用于在 JUnit 4 环境中执行 JUnit 平台上的测试和测试套件的运行器。 有关详细信息,请参阅使用 JUnit 4 运行 JUnit 平台

    junit-platform-suite

    JUnit 平台套件工件,它传递性地引入了对 junit-platform-suite-apijunit-platform-suite-engine 的依赖,以便简化 Gradle 和 Maven 等构建工具中的依赖管理。

    junit-platform-suite-api

    用于在 JUnit 平台上配置测试套件的注解。 由 JUnit 平台套件引擎JUnitPlatform 运行器 支持。

    junit-platform-suite-commons

    用于在 JUnit 平台上执行测试套件的通用支持实用程序。

    junit-platform-suite-engine

    在 JUnit 平台上执行测试套件的引擎; 仅在运行时需要。 有关详细信息,请参阅JUnit 平台套件引擎

    junit-platform-testkit

    提供对给定 TestEngine 执行测试计划的支持,然后通过流畅的 API 访问结果以验证预期结果。

10.2.2. JUnit 木星

  • 组 ID: org.junit.jupiter

  • 版本: 5.12.0

  • 工件 ID:

    junit-jupiter

    JUnit 木星聚合器工件,它传递性地引入了对 junit-jupiter-apijunit-jupiter-paramsjunit-jupiter-engine 的依赖,以便简化 Gradle 和 Maven 等构建工具中的依赖管理。

    junit-jupiter-api

    用于编写测试扩展的 JUnit 木星 API。

    junit-jupiter-engine

    JUnit 木星测试引擎实现; 仅在运行时需要。

    junit-jupiter-params

    支持 JUnit 木星中的参数化测试

    junit-jupiter-migrationsupport

    支持从 JUnit 4 迁移到 JUnit 木星; 仅在支持 JUnit 4 的 @Ignore 注解和运行选定的 JUnit 4 规则时才需要。

10.2.3. JUnit 古董

  • 组 ID: org.junit.vintage

  • 版本: 5.12.0

  • 工件 ID:

    junit-vintage-engine

    JUnit 古董测试引擎实现,允许在 JUnit 平台上运行古董 JUnit 测试。 古董测试包括使用 JUnit 3 或 JUnit 4 API 编写的测试,或使用基于这些 API 构建的测试框架编写的测试。

10.2.4. 物料清单 (BOM)

以下 Maven 坐标下提供的物料清单 POM 可用于在使用 MavenGradle 引用上述多个工件时简化依赖管理。

  • 组 ID: org.junit

  • 工件 ID: junit-bom

  • 版本: 5.12.0

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