1. 概述

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

该文档还提供PDF 版本下载

1.1. 什么是 JUnit 5?

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

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

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

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.gradle,等)。pom.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

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

@AfterEach

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

@BeforeAll

表示被注解的方法应该在当前类中的所有、、、方法之前执行; 类似于 JUnit 4 的. 此类方法是继承的,除非它们被隐藏重写取代(即,仅根据签名进行替换,而不考虑 Java 的可见性规则),而且必须如此,除非使用“每类”测试实例生命周期。 @Test@RepeatedTest@ParameterizedTest@TestFactory@BeforeClassstatic

@AfterAll

表示被注解的方法应该在当前类中的所有、、、方法之后执行; 类似于 JUnit 4 的. 此类方法是继承的,除非它们被隐藏重写取代(即,仅根据签名进行替换,而不考虑 Java 的可见性规则),而且必须如此,除非使用“每类”测试实例生命周期。 @Test@RepeatedTest@ParameterizedTest@TestFactory@AfterClassstatic

@Nested

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

@Tag

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

@Disabled

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

@Timeout

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

@ExtendWith

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

@RegisterExtension

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

@TempDir

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

某些注释目前可能处于实验阶段有关详细信息,请参阅实验 API中的表 。

2.1.1. 元注释和组合注释

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

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

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

import org.junit.jupiter.api.Tag;

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

以下@Test方法演示了注释的用法@Fast

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

您甚至可以更进一步,引入@FastTest可用作 和 的直接替代品的@Tag("fast") @Test定义注释。

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

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

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

JUnit 自动将以下内容识别为@Test带有“快速”标签的方法。

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

2.2. 定义

平台概念
容器

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

测试

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

木星概念
生命周期法

@BeforeAll直接用、@AfterAll@BeforeEach或进行注释或元注释的任何方法 @AfterEach

测试班

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

测试方法

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

2.3. 测试类和方法

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

类和方法的可见性

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

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

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

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

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

class StandardTests {

    @BeforeAll
    static void initAll() {
    }

    @BeforeEach
    void init() {
    }

    @Test
    void succeedingTest() {
    }

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

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

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

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }

}

2.4. 显示名称

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

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

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

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

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

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

}

2.4.1. 显示名称生成器

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

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

显示名称生成器 行为

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. 通过调用注释DisplayNameGenerator中指定的@DisplayNameGeneration (如果存在)

  3. DisplayNameGenerator通过调用通过配置参数配置的默认值(如果存在)

  4. 通过致电org.junit.jupiter.api.DisplayNameGenerator.Standard

2.5. 断言

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

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

import java.util.concurrent.CountDownLatch;

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

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

class AssertionsDemo {

    private final Calculator calculator = new Calculator();

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

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

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

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

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

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

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

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

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

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

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

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

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

}
抢先超时assertTimeoutPreemptively()

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

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

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

2.5.1. Kotlin 断言支持

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

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

class KotlinAssertionsDemo {

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

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

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

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

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

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

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

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

2.5.2. 第三方断言库

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

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

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

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

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class HamcrestAssertionsDemo {

    private final Calculator calculator = new Calculator();

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

}

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

2.6。假设

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

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

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class AssumptionsDemo {

    private final Calculator calculator = new Calculator();

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

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

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

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

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

2.7. 禁用测试

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

这是一个@Disabled测试类。

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

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

    @Test
    void testWillBeSkipped() {
    }

}

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

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

class DisabledTestsDemo {

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

    @Test
    void testWillBeExecuted() {
    }

}

@Disabled可以在不提供理由的情况下宣布;但是,JUnit 团队建议开发人员提供一个简短的解释,说明测试类或测试方法被禁用的原因。因此,上面的例子都显示了原因的使用——例如,@Disabled("Disabled until bug #42 has been resolved")。一些开发团队甚至出于自动可追溯性等原因要求存在问题跟踪编号。

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

2.8. 条件测试执行

JUnit Jupiter 中的扩展ExecutionConditionAPI 允许开发人员以编程方式启用禁用容器或根据某些条件进行测试。这种情况最简单的例子是 支持注释的内置函数(请参阅 禁用测试)。除此之外,JUnit Jupiter 还在包中支持其他几种基于注释的条件 ,允许开发人员以声明方式启用或禁用容器和测试。当注册多个扩展时,一旦其中一个条件返回disabled ,容器或测试就会被禁用。如果您希望提供有关为何禁用它们的详细信息,与这些内置条件关联的每个注释都有一个可用于该目的的属性。DisabledCondition@Disabled@Disabledorg.junit.jupiter.api.conditionExecutionConditiondisabledReason

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

组合注释

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

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

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

2.8.1. 操作系统和架构条件

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

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

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

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

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

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

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

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

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

2.8.2. Java运行环境条件

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

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

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

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

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

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

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

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

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

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

2.8.3. 原生图像条件

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

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

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

2.8.4. 系统属性条件

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

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

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

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

2.8.5。环境变化条件

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

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

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

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

2.8.6。定制条件

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

以下测试类演示了如何配置名为 customConditionvia@EnabledIf和的本地方法@DisabledIf

@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在类级别使用时

  • @EnabledIfor@DisabledIf用于 a@ParameterizedTest@TestTemplate方法时

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

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

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

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

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

2.9. 标记和过滤

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

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

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

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

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

2.10. 测试执行顺序

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

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

2.10.1. 方法顺序

虽然真正的unit测试通常不应该依赖于它们的执行顺序,但有时有必要强制执行特定的测试方法执行顺序 - 例如,在编写集成测试或功能测试时,测试顺序是很重要,尤其是与 @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默认使用 orderer 方法,您应该将配置参数设置为相应的完全限定类名(例如,在 中src/test/resources/junit-platform.properties):

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

同样,您可以指定任何实现 MethodOrderer.

2.10.2. 班级顺序

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

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

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

  • 各种其他用例

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

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

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

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

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

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

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

下面的示例演示了如何保证@Nested测试类按照@Order注释指定的顺序执行。

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

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

    @Nested
    @Order(1)
    class PrimaryTests {

        @Test
        void test1() {
        }
    }

    @Nested
    @Order(2)
    class SecondaryTests {

        @Test
        void test2() {
        }
    }
}

2.11. 测试实例生命周期

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

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

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

与默认的“每个方法”模式相比,“每个类”模式有一些额外的好处。具体来说,通过“每类”模式,可以在非静态方法和接口方法上声明@BeforeAll和 。因此,“每类”模式也使得在测试类中使用和 方法成为可能。@AfterAlldefault@BeforeAll@AfterAll@Nested

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

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

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

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

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

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

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

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

junit.jupiter.testinstance.lifecycle.default = per_class

如果不一致应用,更改默认测试实例生命周期模式可能会导致不可预测的结果和脆弱的构建。例如,如果构建将“每类”语义配置为默认值,但 IDE 中的测试是使用“每方法”语义执行的,则可能会导致调试构建服务器上发生的错误变得困难。因此,建议更改 JUnit 平台配置文件中的默认值,而不是通过 JVM 系统属性。

2.12. 嵌套测试

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

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

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

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

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

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

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

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

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

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

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

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

            String anElement = "an element";

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

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

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

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

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

编写测试嵌套测试IDE
在 IDE 中执行嵌套测试

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

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

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

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

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

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

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

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

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

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

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

@DisplayName("TestInfo Demo")
class TestInfoDemo {

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

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

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

    @Test
    void test2() {
    }

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

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

    在 JUnit Jupiter 中,您应该使用在 JUnit 4 中TestReporter打印信息的位置。 stdout使用会将所有报告的条目输出到. 此外,某些 IDE 还会将报告条目打印到或显示在用户界面中以获取测试结果。stderr@RunWith(JUnitPlatform.class)stdoutstdout

class TestReporterDemo {

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

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

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

        testReporter.publishEntry(values);
    }

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

查看RandomParametersExtension的 自定义示例 ParameterResolver。虽然不打算投入生产,但它展示了扩展模型和参数解析过程的简单性和表现力。MyRandomParametersTest演示如何将随机值注入到@Test 方法中。

@ExtendWith(RandomParametersExtension.class)
class MyRandomParametersTest {

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

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

}

对于实际用例,请查看MockitoExtension和 的 源代码SpringExtension

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

2.14. 测试接口和默认方法

JUnit Jupiter 允许在接口方法上声明@Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate、 、@BeforeEach和 。如果测试接口或测试类带有注释,则可以在测试接口中的方法或接口方法上声明(请参阅 测试实例生命周期)。这里有些例子。@AfterEachdefault@BeforeAll@AfterAllstaticdefault@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.equals或 的实现Comparable.compareTo应如何表现,如下所示。

public interface Testable<T> {

    T createValue();

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

    T createNotEqualValue();

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

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

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

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

    T createSmallerValue();

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

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

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

}

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

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

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

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

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

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

2.15。反复测试

@RepeatedTestJUnit Jupiter 通过注释方法并指定所需的重复总数,提供了重复测试指定次数的能力。每次调用重复测试的行为就像执行常规 @Test方法一样,完全支持相同的生命周期回调和扩展。

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

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

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

例如,如果您经常重复调用您怀疑不稳定的@RepeatedTest测试,则一次失败就足以证明该测试不稳定,并且无需调用其余的重复。要支持该特定用例,请设置. 您也可以根据您的用例将阈值设置为大于 1 的数字。failureThreshold = 1

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

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

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

  • {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 10,repeatedTest() :: repetition 2 of 10等。

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

2.15.1. 重复测试实例

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

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

repeatedTestWithFailureThreshold()演示如何设置故障阈值并模拟每秒重复的意外故障。ConsoleLauncher可以在本节末尾的输出中查看生成的行为。

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

repeatedTestInGerman()演示将重复测试的显示名称翻译成外语的能力 - 在本例中是德语,从而产生单独重复的名称,例如: 、Wiederholung 1 von 5Wiederholung 2 von 5

由于该beforeEach()方法带有注释,@BeforeEach因此它将在每个重复测试的每次重复之前执行。通过将TestInfoRepetitionInfo注入到该方法中,我们发现可以获得有关当前正在执行的重复测试的信息。RepeatedTestsDemo 在启用日志级别的情况下执行会INFO产生以下输出。

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

}

ConsoleLauncher在启用 unicode 主题的情况 下使用时,执行RepeatedTestsDemo结果会在控制台输出以下结果。

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

2.16。参数化测试

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

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

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

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

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

2.16.1. 所需设置

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

2.16.2. 消耗参数

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

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

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

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

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

自动关闭参数

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

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

2.16.3。论据来源

JUnit Jupiter 开箱即用地提供了相当多的注释。以下每个小节都提供了简要概述和示例。请参阅包中的 Javadocorg.junit.jupiter.params.provider了解更多信息。

@ValueSource

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

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

  • short

  • byte

  • int

  • long

  • float

  • double

  • char

  • boolean

  • java.lang.String

  • java.lang.Class

例如,以下@ParameterizedTest方法将被调用三次,值分别为12、 和3

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

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

  • @NullSourcenull:为带注释的方法提供单个参数@ParameterizedTest

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

  • @EmptySource:为以下类型的参数的带注释方法 提供单个参数: , (以及具有无参数构造 函数的具体子类型)、 、、、、 (以及具有无参数构造 函数的具体子类型)、 、、 原始数组(例如, , 等),对象数组(例如,,等)。@ParameterizedTestjava.lang.Stringjava.util.Collectionpublicjava.util.Listjava.util.Setjava.util.SortedSetjava.util.NavigableSetjava.util.Mappublicjava.util.SortedMapjava.util.NavigableMapint[]char[][]String[]Integer[][]

  • @NullAndEmptySource组合注释@NullSource,结合了和的功能 @EmptySource

如果您需要向参数化测试提供多个不同类型的空白字符串,则可以使用@ValueSource来实现 - 例如,@ValueSource(strings = {" ", "   ", "\t", "\n"}).

您还可以组合@NullSource@EmptySource、 和@ValueSource来测试更广泛的nullempty空白输入。以下示例演示了如何对字符串实现此目的。

@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)都会导致 6 次调用:1 次调用null,1 次调用空字符串,4 次调用通过 提供的显式空白字符串@ValueSource
@EnumSource

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

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

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

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

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

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

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

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

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

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

每个工厂方法必须生成一个参数,并且流中的每组参数都将作为带注释方法的单独调用的物理参数提供@ParameterizedTest。一般来说,这会转化为 a Streamof Arguments(即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");
}

还支持原始类型( DoubleStreamIntStream和)的流,如以下示例所示。LongStream

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

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

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

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

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

static class IntegerResolver implements ParameterResolver {

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

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

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

        return 2;
    }

}
@CsvSource

@CsvSource允许您将参数列表表示为逗号分隔值(即 CSV String文字)。value通过in 属性提供的每个字符串@CsvSource 代表一条 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'上例和下表中的值。 除非设置了该属性,否则带引号的空值 ( '') 会导致空值;而完全空的值会被解释为引用。通过指定一个或多个,可以将自定义值解释为参考(请参见下表中的示例)。如果引用的目标类型是原始类型,则会抛出异常 。StringemptyValuenullnullValuesnullNILArgumentConversionExceptionnull

无论通过属性配置任何自定义值,不带引号 的值都将始终转换为引用。 nullnullValues

除带引号的字符串外,默认情况下会修剪 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 或更高版本),您也可以textBlock使用@CsvSource. 文本块中的每条记录代表一条 CSV 记录,并导致参数化测试的一次调用。useHeadersInDisplayName通过将属性设置为,可以选择使用第一条记录来提供 CSV 标头,true如下例所示。

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

@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 属性设置为truenumLinesToSkip下面的示例演示了和 的用法useHeadersInDisplayName

默认分隔符是逗号 ( ,),但您可以通过设置 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);
}
两列.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"上面示例中的值。除非 设置了该属性,否则带引号的空值 ( "") 会导致空值;而完全空的值会被解释为 引用。通过指定一个或多个,可以将自定义值解释为参考。如果引用的目标类型是原始类型,则会抛出异常。StringemptyValuenullnullValuesnullArgumentConversionExceptionnull

无论通过属性配置任何自定义值,不带引号 的值都将始终转换为引用。 nullnullValues

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

@ArgumentsSource

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

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

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

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

2.16.4。参数转换

扩大转换

JUnit Jupiter 支持 对提供@ParameterizedTest. 例如,用 注释的参数化测试@ValueSource(ints = { 1, 2, 3 })可以声明为不仅接受 类型的参数int,还接受 、 或 类型longfloat参数double

隐式转换

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

例如,如果 a@ParameterizedTest声明了一个类型的参数TimeUnit,并且声明的源提供的实际类型是 a 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.org/"URI.create("https://junit.org/")

java.net.URL

"https://junit.org/"URI.create("https://junit.org/").toURL()

java.nio.charset.Charset

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

java.nio.file.Path

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

java.time.Duration

"PT3S"Duration.ofSeconds(3)

java.time.Instant

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

java.time.LocalDateTime

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

java.time.LocalDate

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

java.time.LocalTime

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

java.time.MonthDay

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

java.time.OffsetDateTime

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

java.time.OffsetTime

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

java.time.Period

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

java.time.YearMonth

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

java.time.Year

"2017"Year.of(2017)

java.time.ZonedDateTime

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

java.time.ZoneId

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

java.time.ZoneOffset

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

java.util.Currency

"JPY"Currency.getInstance("JPY")

java.util.Locale

"en"new Locale("en")

java.util.UUID

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

回退字符串到对象的转换

除了从字符串到上表中列出的目标类型的隐式转换之外,JUnit Jupiter 还提供了一种后备机制, String如果目标类型恰好声明了一个合适的工厂方法或定义的工厂构造函数,则可以自动从 转换为给定的目标类型以下。

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

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

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

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

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

    private final String title;

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

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

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

您可以使用注释显式指定 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.16.5。参数聚合

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

ArgumentsAccessor在这种情况下,可以使用an来代替多个参数。使用此 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

定制聚合器

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

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

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

如果您发现自己@AggregateWith(MyTypeAggregator.class)在代码库中重复声明多个参数化测试方法,您可能希望创建一个自定义 组合注释,例如@CsvToMyType使用 @AggregateWith(MyTypeAggregator.class). 以下示例通过自定义注释演示了这一点@CsvToPerson

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

2.16.6。自定义显示名称

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

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

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

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

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

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

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

占位符 描述

{displayName}

方法的显示名称

{index}

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

{arguments}

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

{argumentsWithNames}

包含参数名称的完整的、以逗号分隔的参数列表

{0},, {1}...

个人论点

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

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

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

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

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

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

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

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

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

  3. DEFAULT_DISPLAY_NAME常数定义在@ParameterizedTest

2.16.7。生命周期和互操作性

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

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

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

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

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

2.17。测试模板

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

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

2.18。动态测试

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

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

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

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

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

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

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

从 JUnit Jupiter 5.10.1 开始,动态测试必须始终通过工厂方法创建;然而,这可能会在以后的版本中通过注册工具来补充。

2.18.1. 动态测试示例

下面的DynamicTestsDemo课程演示了测试工厂和动态测试的几个示例。

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

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

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

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

出于演示目的,该dynamicNodeSingleTest()方法生成单个 DynamicTest流而不是流,并且该dynamicNodeSingleContainer()方法利用 生成动态测试的嵌套层次结构DynamicContainer

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

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

import example.util.Calculator;

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

class DynamicTestsDemo {

    private final Calculator calculator = new Calculator();

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

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

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

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

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

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

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

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {

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

            Random random = new Random();
            int current;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

2.18.2. 动态测试的 URI 测试源

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

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

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[]). 请参阅 Javadoc 了解DiscoverySelectors.selectMethod(String)FQMN 支持的格式。

ClassSource

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

UriSource

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

2.19。超时

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

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

class TimeoutDemo {

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

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

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

}

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

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

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

2.19.1。线程模式

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

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

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

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

2.19.2. 默认超时

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

junit.jupiter.execution.timeout.default

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

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

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

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

@Test方法的默认超时

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

@TestTemplate方法的默认超时

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

@TestFactory方法的默认超时

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

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

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

@BeforeAll方法的默认超时

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

@BeforeEach方法的默认超时

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

@AfterEach方法的默认超时

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

@AfterAll方法的默认超时

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

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

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

42

@Timeout(42)

42 ns

@Timeout(value = 42, unit = NANOSECONDS)

42 μs

@Timeout(value = 42, unit = MICROSECONDS)

42 ms

@Timeout(value = 42, unit = MILLISECONDS)

42 s

@Timeout(value = 42, unit = SECONDS)

42 m

@Timeout(value = 42, unit = MINUTES)

42 h

@Timeout(value = 42, unit = HOURS)

42 d

@Timeout(value = 42, unit = DAYS)

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

在处理异步代码时,通常会编写轮询测试,同时等待某些事情发生,然后再执行任何断言。在某些情况下,您可以重写逻辑以使用一种CountDownLatch或另一种同步机制,但有时这是不可能的 - 例如,如果被测试的主题将消息发送到外部消息代理中的通道,并且在该消息之前无法执行断言已通过通道成功发送。像这样的异步测试需要某种形式的超时,以确保它们不会无限期地执行而挂起测试套件,就像异步消息从未成功传递的情况一样。

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

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

2.19.4。全局禁用@Timeout

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

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

2.20. 并行执行

默认情况下,JUnit Jupiter 测试在单个线程中按顺序运行。从 5.3 版开始,并行运行测试(例如,为了加快执行速度)作为一项可选功能提供。要启用并行执行,请将 junit.jupiter.execution.parallel.enabled配置参数设置为true — 例如 in 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或 a 的 测试类MethodOrderer(除了MethodOrderer.Random)。在前一种情况下,测试作者必须确保测试类是线程安全的;在后者中,并发执行可能与配置的执行顺序冲突。@Execution(CONCURRENT) 因此,在这两种情况下,只有在测试类或方法上存在注释时,此类测试类中的测试方法才会并发执行。

当启用并行执行并ClassOrderer注册默认值时(有关详细信息,请参阅 类顺序),顶级测试类最初将相应地排序并按该顺序安排。但是,不能保证它们完全按照该顺序启动,因为执行它们的线程不是由 JUnit 直接控制的。

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

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

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

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

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

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

编写测试执行模式
默认执行模式配置组合

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

2.20.1。配置

可以使用 来配置所需的并行性和最大池大小等属性ParallelExecutionConfigurationStrategy。JUnit 平台提供了两种开箱即用的实现: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

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

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

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

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

财产 描述 支持的值 默认值

junit.jupiter.execution.parallel.enabled

启用并行测试执行

  • true

  • false

false

junit.jupiter.execution.parallel.mode.default

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

  • concurrent

  • same_thread

same_thread

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

顶级类的默认执行模式

  • concurrent

  • same_thread

same_thread

junit.jupiter.execution.parallel.config.strategy

所需并行性和最大池大小的执行策略

  • dynamic

  • fixed

  • custom

dynamic

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

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

正十进制数

1.0

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

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

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

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

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

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

例如,org.example.CustomStrategy

无默认值

2.20.2. 同步

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

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

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

隔离运行测试

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

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

@Execution(CONCURRENT)
class SharedResourcesDemo {

    private Properties backup;

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

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

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

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

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

}

2.21。内置扩展

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

2.21.1。临时目录扩展

内置TempDirectory扩展用于为单个测试或测试类中的所有测试创建和清理临时目录。默认情况下已注册。要使用它,请注释一个非最终的、未分配的 type java.nio.file.Pathor java.io.Filewith字段,或者将一个 type or 注释@TempDir的参数添加到生命周期方法或测试方法中。java.nio.file.Pathjava.io.File@TempDir

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

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

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

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

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

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

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

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

@TempDir构造函数参数不支持。如果您希望跨生命周期方法和当前测试方法保留对临时目录的单个引用,请通过使用@TempDir.

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

跨测试方法共享临时目录的测试类
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::createTempDirectoryjunit并作为用于生成目录名称的前缀字符串传递。

以下示例定义了一个使用测试名称作为目录名称前缀而不是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 可能会公开附加属性来配置工厂。AnnotatedElementContext 此类注释和相关属性可以通过的参数访问createTempDirectory

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

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

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

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

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

3. 从 JUnit 4 迁移

尽管 JUnit Jupiter 编程模型和扩展模型本身不支持 JUnit 4 功能,例如RulesRunners,但预计源代码维护者不需要更新所有现有测试、测试扩展和自定义构建测试基础架构以迁移到 JUnit木星。

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

3.1. 在 JUnit 平台上运行 JUnit 4 测试

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

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

3.1.1. 类别 支持

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

3.2. 迁移技巧

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

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

  • 断言驻留在org.junit.jupiter.api.Assertions.

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

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

    • 请注意,JUnit Jupiter 5.4 及更高版本支持 JUnit 4 org.junit.Assume类中的方法进行假设。具体来说,JUnit Jupiter 支持 JUnit 4AssumptionViolatedException来发出信号,表明测试应该中止,而不是标记为失败。

  • @Before并且@After不再存在;使用@BeforeEachand@AfterEach代替。

  • @BeforeClass并且@AfterClass不再存在;使用@BeforeAlland@AfterAll 代替。

  • @Ignore不再存在:使用@Disabled或其他内置 执行条件之一代替

  • @Category不复存在; @Tag代替使用。

  • @RunWith不复存在; 被 取代@ExtendWith

  • @Rule并且@ClassRule不再存在;@ExtendWith被和 取代@RegisterExtension

  • @Test(expected = …​)并且ExpectedException规则不再存在;Assertions.assertThrows(…​)代替使用 。

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

3.3. 有限的 JUnit 4 规则支持

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

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

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

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

  • org.junit.rules.ExpectedException

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

这种有限形式的Rule支持可以通过类级注释来打开 @EnableRuleMigrationSupport。此注释是一个组合注释,可启用所有规则迁移支持扩展:VerifierSupportExternalResourceSupportExpectedExceptionSupport。您也可以选择注释您的测试类,用于 @EnableJUnit4MigrationSupport注册规则JUnit 4@Ignore注释的迁移支持(请参阅JUnit 4 @Ignore Support)。

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

3.4. JUnit 4 @忽略支持

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

@Ignore要与基于 JUnit Jupiter 的测试一起使用,请在构建中配置对 模块的测试junit-jupiter-migrationsupport依赖项,然后使用@ExtendWith(IgnoreCondition.class)或注释您的测试类@EnableJUnit4MigrationSupport(这会自动注册IgnoreCondition以及 Limited JUnit 4 Rule Support)。是IgnoreCondition禁用 ExecutionCondition用 注释的测试类或测试方法的 @Ignore

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

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

    @Ignore
    @Test
    void testWillBeIgnored() {
    }

    @Test
    void testWillBeExecuted() {
    }
}

3.5. 失败消息参数

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

例如,JUnit 4 中的方法assertEquals被声明为assertEquals(String message, Object expected, Object actual),但在 JUnit Jupiter 中它被声明为 assertEquals(Object expected, Object actual, String message)。这样做的理由是失败消息是可选的,并且可选参数应该在方法签名中的必需参数之后声明。

受此更改影响的方法如下:

  • 断言

    • assertTrue

    • assertFalse

    • assertNull

    • assertNotNull

    • assertEquals

    • assertNotEquals

    • assertArrayEquals

    • assertSame

    • assertNotSame

    • assertThrows

  • 假设

    • assumeTrue

    • assumeFalse

4. 运行测试

4.1. IDE支持

4.1.1. 智能IDEA

自 2016.2 版本起,IntelliJ IDEA 支持在 JUnit 平台上运行测试。有关详细信息,请参阅 IntelliJ IDEA 博客上的帖子。但请注意,建议使用 IDEA 2017.3 或更高版本,因为这些较新版本的 IDEA 会根据项目中使用的 API 版本自动下载以下 JAR:junit-platform-launcherjunit-jupiter-enginejunit-vintage-engine

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

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

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

4.1.2. 蚀

自 Eclipse Oxygen.1a (4.7.1a) 发布以来,Eclipse IDE 提供了对 JUnit 平台的支持。

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

4.1.3. 网豆

自Apache NetBeans 10.0 发布以来,NetBeans 就提供了对 JUnit Jupiter 和 JUnit Platform 的支持 。

4.1.4. 视觉工作室代码

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

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

4.1.5. 其他 IDE

如果您使用的编辑器或 IDE 不是前面几节中列出的编辑器或 IDE,JUnit 团队提供了两种替代解决方案来帮助您使用 JUnit 5。您可以手动使用控制台启动器(例如,从命令行)或者如果您的 IDE 内置了对 JUnit 4 的支持,则使用基于 JUnit 4 的 Runner执行测试。

4.2. 建立支持

4.2.1. 摇篮

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

test {
    useJUnitPlatform()
}

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

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

请参阅 官方 Gradle 文档 以获取完整的选项列表。

调整依赖版本

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

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

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

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

标准 Gradletest任务当前不提供专用 DSL 来设置 JUnit 平台配置参数以影响测试发现和执行。但是,您可以通过系统属性(如下所示)或通过文件在构建脚本中提供配置参数 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 的测试的支持,请配置testImplementation对依赖项聚合 JUnit Jupiter 工件的依赖项,如下所示。

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

只要您配置testImplementation 对 JUnit 4 的依赖项和testRuntimeOnly对 JUnit VintageTestEngine 实现的依赖项(类似于以下内容),JUnit 平台就可以运行基于 JUnit 4 的测试。

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

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

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

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

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

4.2.2. 梅文

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

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

Maven Surefire/Failsafe 3.0.0-M4 引入了对将其使用的 JUnit 平台启动器版本与测试运行时类路径上找到的 JUnit 平台版本进行对齐的支持。因此,建议使用3.0.0-M4或更高版本,以避免互操作性问题。

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

<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.10.1</version>
    <scope>test</scope>
</dependency>
调整依赖版本

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

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.10.1</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 的测试的支持,请配置testJUnit Jupiter API 和 JUnit JupiterTestEngine实现的范围依赖项,如下所示。

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

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

<!-- ... -->
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.10.1</version> <!-- can be omitted when using the BOM -->
        <scope>test</scope>
    </dependency>
    <!-- ... -->
</dependencies>
<!-- ... -->
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.1.2</version>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>3.1.2</version>
        </plugin>
    </plugins>
</build>
<!-- ... -->
按测试类名称过滤

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

  • **/Test*.java

  • **/*Test.java

  • **/*Tests.java

  • **/*TestCase.java

而且,它默认会排除所有嵌套类(包括静态成员类)。

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

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

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

按标签过滤

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

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

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

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

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

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

4.2.3. 蚂蚁

从 version 开始1.10.3Ant有一个 junitlauncher任务为在 JUnit 平台上启动测试提供本机支持。该junitlauncher 任务仅负责启动 JUnit 平台并向其传递选定的测试集合。然后,JUnit 平台委托注册的测试引擎来发现和执行测试。

junitlauncher任务尝试尽可能与本机 Ant 构造(例如 资源集合)保持一致 ,以允许用户选择他们希望由测试引擎执行的测试。与许多其他核心 Ant 任务相比,这使该任务具有一致和自然的感觉。

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

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

基本用法

以下示例演示如何配置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 平台的类路径。该类路径还将用于定位作为执行一部分的测试类。

以下示例演示如何配置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提供自动支持来管理项目中使用的 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.versionGradleMaven都记录了更改依赖项版本的机制 。

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

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

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

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

4.3. 控制台启动器

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

包含所有依赖项的可执行文件junit-platform-console-standalone-1.10.1.jar发布在Maven 中央存储库的 junit-platform-console-standalone目录下 。它包括以下依赖项:

  • junit:junit:4.13.2

  • org.apiguardian:apiguardian-api:1.1.2

  • org.hamcrest:hamcrest-core:1.3

  • org.junit.jupiter:junit-jupiter-api:5.10.1

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

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

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

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

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

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

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

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

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

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

  • org.junit.platform:junit-platform-suite:1.10.1

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

  • org.opentest4j:opentest4j:1.3.0

您可以运行独立程序ConsoleLauncher,如下所示。

$ java -jar junit-platform-console-standalone-1.10.1.jar execute <OPTIONS>

├─ JUnit Vintage
│  └─ example.JUnit4Tests
│     └─ standardJUnit4Test ✔
└─ JUnit Jupiter
   ├─ StandardTests
   │  ├─ succeedingTest() ✔
   │  └─ skippedTest() ↷ for demonstration purposes
   └─ A special test case
      ├─ Custom test name containing spaces ✔
      ├─ ╯°□°)╯ ✔
      └─ ???? ✔

Test run finished after 64 ms
[         5 containers found      ]
[         0 containers skipped    ]
[         5 containers started    ]
[         0 containers aborted    ]
[         5 containers successful ]
[         0 containers failed     ]
[         6 tests found           ]
[         1 tests skipped         ]
[         5 tests started         ]
[         0 tests aborted         ]
[         5 tests successful      ]
[         0 tests failed          ]

您还可以运行独立程序ConsoleLauncher,如下所示(例如,包含目录中的所有 jar):

$ java -cp classes:testlib/* org.junit.platform.console.ConsoleLauncher <OPTIONS>
退出代码
如果任何容器或测试失败,则 退出ConsoleLauncher并带有状态代码。1如果未发现任何测试并且--fail-if-no-tests提供了命令行选项,则ConsoleLauncher退出并显示状态代码2。否则,退出代码为0

4.3.1. 子命令和选项

提供ConsoleLauncher以下子命令:

 Usage: junit [OPTIONS] [COMMAND]
Launches the JUnit Platform for test discovery and execution.
      [@<filename>...]   One or more argument files containing options.
Commands:
  discover  Discover tests
  execute   Execute tests
  engines   List available test engines

For more information, please refer to the JUnit User Guide at
https://junit.org/junit5/docs/current/user-guide/
发现测试
 Usage: junit discover [OPTIONS]
Discover tests
      [@<filename>...]       One or more argument files containing options.
      --disable-banner       Disable print out of the welcome message.
      --disable-ansi-colors  Disable ANSI colors in output (not supported by all terminals).
  -h, --help                 Display help information.

SELECTORS

      --scan-classpath, --scan-class-path[=PATH]
                             Scan all directories on the classpath or explicit classpath
                               roots. Without arguments, only directories on the system
                               classpath as well as additional classpath entries supplied via
                               -cp (directories and JAR files) are scanned. Explicit classpath
                               roots that are not on the classpath will be silently ignored.
                               This option can be repeated.
      --scan-modules         Scan all resolved modules for test discovery.
  -u, --select-uri=URI       Select a URI for test discovery. This option can be repeated.
  -f, --select-file=FILE     Select a file for test discovery. This option can be repeated.
  -d, --select-directory=DIR Select a directory for test discovery. This option can be
                               repeated.
  -o, --select-module=NAME   Select single module for test discovery. This option can be
                               repeated.
  -p, --select-package=PKG   Select a package for test discovery. This option can be repeated.
  -c, --select-class=CLASS   Select a class for test discovery. This option can be repeated.
  -m, --select-method=NAME   Select a method for test discovery. This option can be repeated.
  -r, --select-resource=RESOURCE
                             Select a classpath resource for test discovery. This option can
                               be repeated.
  -i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]
                             Select iterations for test discovery (e.g. method:com.acme.Foo#m()
                               [1..2]). This option can be repeated.

FILTERS

  -n, --include-classname=PATTERN
                             Provide a regular expression to include only classes whose fully
                               qualified names match. To avoid loading classes unnecessarily,
                               the default pattern only includes class names that begin with
                               "Test" or end with "Test" or "Tests". When this option is
                               repeated, all patterns will be combined using OR semantics.
                               Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$
  -N, --exclude-classname=PATTERN
                             Provide a regular expression to exclude those classes whose fully
                               qualified names match. When this option is repeated, all
                               patterns will be combined using OR semantics.
      --include-package=PKG  Provide a package to be included in the test run. This option can
                               be repeated.
      --exclude-package=PKG  Provide a package to be excluded from the test run. This option
                               can be repeated.
  -t, --include-tag=TAG      Provide a tag or tag expression to include only tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -T, --exclude-tag=TAG      Provide a tag or tag expression to exclude those tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -e, --include-engine=ID    Provide the ID of an engine to be included in the test run. This
                               option can be repeated.
  -E, --exclude-engine=ID    Provide the ID of an engine to be excluded from the test run.
                               This option can be repeated.

RUNTIME CONFIGURATION

      -cp, --classpath, --class-path=PATH
                             Provide additional classpath entries -- for example, for adding
                               engines and their dependencies. This option can be repeated.
      --config=KEY=VALUE     Set a configuration parameter for test discovery and execution.
                               This option can be repeated.

CONSOLE OUTPUT

      --color-palette=FILE   Specify a path to a properties file to customize ANSI style of
                               output (not supported by all terminals).
      --single-color         Style test output using only text attributes, no color (not
                               supported by all terminals).
      --details=MODE         Select an output details mode for when tests are executed. Use
                               one of: none, summary, flat, tree, verbose, testfeed. If 'none'
                               is selected, then only the summary and test failures are shown.
                               Default: tree.
      --details-theme=THEME  Select an output details tree theme for when tests are executed.
                               Use one of: ascii, unicode. Default is detected based on
                               default character encoding.

For more information, please refer to the JUnit User Guide at
https://junit.org/junit5/docs/current/user-guide/
执行测试
 Usage: junit execute [OPTIONS]
Execute tests
      [@<filename>...]       One or more argument files containing options.
      --disable-banner       Disable print out of the welcome message.
      --disable-ansi-colors  Disable ANSI colors in output (not supported by all terminals).
  -h, --help                 Display help information.

SELECTORS

      --scan-classpath, --scan-class-path[=PATH]
                             Scan all directories on the classpath or explicit classpath
                               roots. Without arguments, only directories on the system
                               classpath as well as additional classpath entries supplied via
                               -cp (directories and JAR files) are scanned. Explicit classpath
                               roots that are not on the classpath will be silently ignored.
                               This option can be repeated.
      --scan-modules         Scan all resolved modules for test discovery.
  -u, --select-uri=URI       Select a URI for test discovery. This option can be repeated.
  -f, --select-file=FILE     Select a file for test discovery. This option can be repeated.
  -d, --select-directory=DIR Select a directory for test discovery. This option can be
                               repeated.
  -o, --select-module=NAME   Select single module for test discovery. This option can be
                               repeated.
  -p, --select-package=PKG   Select a package for test discovery. This option can be repeated.
  -c, --select-class=CLASS   Select a class for test discovery. This option can be repeated.
  -m, --select-method=NAME   Select a method for test discovery. This option can be repeated.
  -r, --select-resource=RESOURCE
                             Select a classpath resource for test discovery. This option can
                               be repeated.
  -i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]
                             Select iterations for test discovery (e.g. method:com.acme.Foo#m()
                               [1..2]). This option can be repeated.

FILTERS

  -n, --include-classname=PATTERN
                             Provide a regular expression to include only classes whose fully
                               qualified names match. To avoid loading classes unnecessarily,
                               the default pattern only includes class names that begin with
                               "Test" or end with "Test" or "Tests". When this option is
                               repeated, all patterns will be combined using OR semantics.
                               Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$
  -N, --exclude-classname=PATTERN
                             Provide a regular expression to exclude those classes whose fully
                               qualified names match. When this option is repeated, all
                               patterns will be combined using OR semantics.
      --include-package=PKG  Provide a package to be included in the test run. This option can
                               be repeated.
      --exclude-package=PKG  Provide a package to be excluded from the test run. This option
                               can be repeated.
  -t, --include-tag=TAG      Provide a tag or tag expression to include only tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -T, --exclude-tag=TAG      Provide a tag or tag expression to exclude those tests whose tags
                               match. When this option is repeated, all patterns will be
                               combined using OR semantics.
  -e, --include-engine=ID    Provide the ID of an engine to be included in the test run. This
                               option can be repeated.
  -E, --exclude-engine=ID    Provide the ID of an engine to be excluded from the test run.
                               This option can be repeated.

RUNTIME CONFIGURATION

      -cp, --classpath, --class-path=PATH
                             Provide additional classpath entries -- for example, for adding
                               engines and their dependencies. This option can be repeated.
      --config=KEY=VALUE     Set a configuration parameter for test discovery and execution.
                               This option can be repeated.

CONSOLE OUTPUT

      --color-palette=FILE   Specify a path to a properties file to customize ANSI style of
                               output (not supported by all terminals).
      --single-color         Style test output using only text attributes, no color (not
                               supported by all terminals).
      --details=MODE         Select an output details mode for when tests are executed. Use
                               one of: none, summary, flat, tree, verbose, testfeed. If 'none'
                               is selected, then only the summary and test failures are shown.
                               Default: tree.
      --details-theme=THEME  Select an output details tree theme for when tests are executed.
                               Use one of: ascii, unicode. Default is detected based on
                               default character encoding.

REPORTING

      --fail-if-no-tests     Fail and return exit status code 2 if no tests are found.
      --reports-dir=DIR      Enable report output into a specified local directory (will be
                               created if it does not exist).

For more information, please refer to the JUnit User Guide at
https://junit.org/junit5/docs/current/user-guide/
列出测试引擎
 Usage: junit engines [OPTIONS]
List available test engines
      [@<filename>...]   One or more argument files containing options.
      --disable-banner   Disable print out of the welcome message.
      --disable-ansi-colors
                         Disable ANSI colors in output (not supported by all terminals).
  -h, --help             Display help information.

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

4.3.2. 参数文件(@-文件)

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

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

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

如果参数文件不存在或无法读取,则参数将按字面意思处理,不会被删除。这可能会导致“参数不匹配”错误消息。picocli.trace您可以通过在系统属性设置为 的情况下执行命令来解决此类错误 DEBUG

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

@您可以通过使用附加符号对其进行转义来传递带有初始字符的实际参数@。例如,@@somearg将成为@somearg且不会受到扩张。

4.3.3. 颜色定制

输出中使用的颜色ConsoleLauncher可以自定义。该选项--single-color将应用内置的单色样式,同时 --color-palette接受属性文件来覆盖 ANSI SGR颜色样式。下面的属性文件演示了默认样式:

SUCCESSFUL = 32
ABORTED = 33
FAILED = 31
SKIPPED = 35
CONTAINER = 35
TEST = 34
DYNAMIC = 35
REPORTED = 37

4.4. 使用 JUnit 4 运行 JUnit 平台

跑步JUnitPlatform者已被弃用

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

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

此外,模块@Suite提供的支持 的引入junit-platform-suite-engine使得JUnitPlatform运行器变得过时。有关详细信息,请参阅 JUnit 平台套件引擎

因此,运行JUnitPlatform器和@UseTechnicalNames注释在 JUnit Platform 1.8 中已被弃用,并将在 JUnit Platform 2.0 中删除。

如果您正在使用JUnitPlatform运行程序,请迁移到@Suite支持。

JUnitPlatform运行器是基于 JUnit 4 的Runner,它使您能够在 JUnit 4 环境中的 JUnit 平台上运行其编程模型受支持的任何测试 - 例如,JUnit Jupiter 测试类。

使用 注释类@RunWith(JUnitPlatform.class)可以使其在 IDE 中运行并构建支持 JUnit 4 但尚不直接支持 JUnit 平台的系统。

由于 JUnit 平台具有 JUnit 4 所没有的功能,因此运行程序只能支持 JUnit 平台功能的子集,特别是在报告方面(请参阅显示名称与技术名称)。

4.4.1. 设置

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

显式依赖关系
  • junit-platform-runner测试范围:跑步者的位置JUnitPlatform

  • junit-4.13.2.jar测试范围内:使用 JUnit 4 运行测试

  • junit-jupiter-api测试范围内:使用 JUnit Jupiter 编写测试的 API,包括@Test等。

  • junit-jupiter-engine测试运行时TestEngine范围内: JUnit Jupiter 的 API实现

传递依赖
  • junit-platform-suite-api测试范围内

  • junit-platform-suite-commons测试范围内

  • junit-platform-launcher测试范围内

  • junit-platform-engine测试范围内

  • junit-platform-commons测试范围内

  • opentest4j测试范围内

4.4.2. 显示名称与技术名称

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

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

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

4.4.3. 单一测试班

使用JUnitPlatform运行器的一种方法是直接使用注释测试类 @RunWith(JUnitPlatform.class)org.junit.jupiter.api.Test请注意,以下示例中的测试方法用(JUnit Jupiter) 注释,而不是org.junit.Test(JUnit 4)。此外,在这种情况下,测试类必须是public;否则,某些 IDE 和构建工具可能无法将其识别为 JUnit 4 测试类。

import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;

@RunWith(org.junit.platform.runner.JUnitPlatform.class)
public class JUnitPlatformClassDemo {

    @Test
    void succeedingTest() {
        /* no-op */
    }

    @Test
    void failingTest() {
        fail("Failing for failing's sake.");
    }

}

4.4.4. 测试套件

如果您有多个测试类,您可以创建一个测试套件,如以下示例所示。

import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.runner.RunWith;

@RunWith(org.junit.platform.runner.JUnitPlatform.class)
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
public class JUnitPlatformSuiteDemo {
}

将发现并运行包及其子包JUnitPlatformSuiteDemo中的所有测试。example默认情况下,它只包含名称以 or 开头或Test结尾的测试类。TestTests

附加配置选项
有更多的配置选项用于发现和过滤测试,而不仅仅是 @SelectPackages. 请参阅包的 Javadocorg.junit.platform.suite.api了解更多详细信息。
注释的测试类和套件@RunWith(JUnitPlatform.class) 不能直接在 JUnit 平台上执行(或作为某些 IDE 中记录的“JUnit 5”测试)。此类类和套件只能使用 JUnit 4 基础设施来执行。

4.5. 配置参数

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

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

  1. 其中的 和 方法 用于configurationParameter()构建提供给API 的请求 。通过 JUnit 平台提供的工具之一运行测试时,您可以指定配置参数,如下所示:configurationParameters()LauncherDiscoveryRequestBuilderLauncher

  2. JVM 系统属性。

  3. junit-platform.propertiesJUnit Platform 配置文件:在类路径根目录中命名的文件,遵循 JavaProperties文件的语法规则。

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

4.5.1. 模式匹配语法

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

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

例子:

  • *:匹配所有候选类别。

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

  • *.MyCustomImpl:匹配简单类名恰好为 的每个候选类 MyCustomImpl

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

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

  • org.example.MyCustomImpl:匹配 FQCN 恰好为 的候选类 org.example.MyCustomImpl

  • org.example.MyCustomImpl, org.example.TheirCustomImplorg.example.MyCustomImpl:匹配 FQCN 恰好为 或的候选类org.example.TheirCustomImpl

4.6. 标签

标签是用于标记和过滤测试的 JUnit 平台概念。向容器和测试添加标签的编程模型由测试框架定义。例如,在基于 JUnit Jupiter 的测试中,应使用@Tag注释(请参阅 标记和过滤)。对于基于 JUnit 4 的测试,Vintage 引擎将@Category注释映射到标签(请参阅 类别支持)。其他测试框架可能会定义自己的注释或其他方式供用户指定标签。

4.6.1. 标签的语法规则

无论如何指定标签,JUnit 平台都会强制执行以下规则:

  • 标签不能为null或 为

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

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

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

    • ,:逗号

    • (:左括号

    • ):右括号

    • &:和符号

    • |:竖线

    • !:感叹号

在上面的上下文中,“修剪”意味着前导和尾随空白字符已被删除。

4.6.2. 标签表达式

标签表达式是带有运算符!,&和的布尔表达式|。此外, ()可用于调整运算符优先级。

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

表 2. 运算符(按优先级降序排列)
操作员 意义 关联性

!

不是

正确的

&

左边

|

或者

左边

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

标签表达 选择

product

产品的所有测试

catalog | shipping

所有目录测试以及所有运输测试

catalog & shipping

目录运输之间交叉的所有测试

product & !end-to-end

产品的所有测试,但不是端到端测试

(micro | integration) & (product | shipping)

产品运输的所有微观集成测试

4.7. 捕获标准输出/错误

System.out从版本 1.3 开始,JUnit 平台提供了对捕获打印到和 的输出的选择性支持System.err。要启用它,请将 junit.platform.output.capture.stdout和/或junit.platform.output.capture.stderr 配置参数设置为true。此外,您可以使用 来配置每个执行的测试或容器使用的最大缓冲字节数junit.platform.output.capture.maxBuffer

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

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

4.8. 使用监听器和拦截器

JUnit 平台提供以下侦听器 API,允许 JUnit、第三方和自定义用户代码对发现和执行TestPlan.

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

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

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

JUnit 平台提供了以下侦听器,您可能希望将它们与测试套件一起使用。

JUnit 平台报告

LegacyXmlReportGeneratingListener可以通过控制台启动器使用 或手动注册来生成与基于 JUnit 4 的测试报告的事实标准兼容的 XML 报告。

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

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

飞行记录仪支持

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

LoggingListener

TestExecutionListenerBiConsumer用于通过使用Throwable和 的来记录所有事件的信息消息 Supplier<String>

SummaryGeneratingListener

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

UniqueIdTrackingListener

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

4.8.1. 飞行记录仪支持

从版本 1.7 开始,JUnit 平台提供了对生成 Flight Recorder 事件的选择支持。JEP 328将 Java 飞行记录器 (JFR) 描述为:

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

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

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

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

  3. 启动试运行时开始飞行记录。可以通过 java 命令行选项启动 Flight Recorder:

     -XX:StartFlightRecording:filename=...

请查阅构建工具的手册以获取适当的命令。

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

飞行记录器支持目前是一项实验性功能。我们邀请您尝试一下并向 JUnit 团队提供反馈,以便他们改进并最终 推广此功能。

4.9. 堆栈跟踪修剪

从版本 1.10 开始,JUnit 平台提供了对修剪失败测试所产生的堆栈跟踪的内置支持。该功能默认启用,但可以通过将junit.platform.stacktrace.pruning.enabled 配置参数设置为 来 禁用false

启用后,来自org.junitjdk.internal.reflectsun.reflect 包的所有调用都会从堆栈跟踪中删除,除非调用发生在测试本身或其任何祖先之后。因此,永远不会排除拨打org.junit.jupiter.api.Assertions或 的电话。org.junit.jupiter.api.Assumptions

此外,JUnit 平台启动器第一次调用之前的所有元素(包括 JUnit 平台启动器的第一次调用)都将被删除。

5. 扩展模型

5.1. 概述

与 JUnit 4 中竞争的RunnerTestRule、 和MethodRule扩展点相比,JUnit Jupiter 扩展模型由一个单一的、连贯的概念组成:API Extension。但请注意,它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将按照它们在源代码中声明的顺序执行。例如,在 和 中测试的执行MyFirstTests将由和MySecondTests扩展,完全按照该顺序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生成随机数,这些随机数可以注入到字段中或通过构造函数、测试方法或生命周期方法中的参数注入。如果扩展提供了@Random元注释的注释 @ExtendWith(RandomNumberExtension.class)(请参阅下面的列表),则可以透明地使用扩展,如下RandomNumberDemo例所示。

@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {

    // Use static randomNumber0 field anywhere in the test class,
    // including @BeforeAll or @AfterEach lifecycle methods.
    @Random
    private static Integer randomNumber0;

    // Use randomNumber1 field in test methods and @BeforeEach
    // or @AfterEach lifecycle methods.
    @Random
    private int randomNumber1;

    RandomNumberDemo(@Random int randomNumber2) {
        // Use randomNumber2 in constructor.
    }

    @BeforeEach
    void beforeEach(@Random int randomNumber3) {
        // Use randomNumber3 in @BeforeEach method.
    }

    @Test
    void test(@Random int randomNumber4) {
        // Use randomNumber4 in test method.
    }

}

下面的代码清单提供了一个示例,说明人们可以如何选择实现这样的 RandomNumberExtension. 此实现适用于以下用例 RandomNumberDemo:然而,它可能不足以覆盖所有用例——例如,随机数生成支持仅限于整数;它使用 java.util.Random而不是java.security.SecureRandom; 无论如何,重要的是要注意实现了哪些扩展 API 以及实现的原因。

具体来说,RandomNumberExtension实现了以下扩展API:

  • BeforeAllCallback:支持静态场注入

  • BeforeEachCallback:支持非静态场注入

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

理想情况下,应该在测试类实例化后立即RandomNumberExtension实现TestInstancePostProcessor,而不是为了支持非静态字段注入。BeforeEachCallback

但是,JUnit Jupiter 目前不允许在非静态字段上 TestInstancePostProcessor注册 via (请参阅问题 3437)。有鉴于此, 实施作为替代方法。@ExtendWithRandomNumberExtensionBeforeEachCallback

import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields;

import java.lang.reflect.Field;
import java.util.function.Predicate;

import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.ModifierSupport;

class RandomNumberExtension
        implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {

    private final java.util.Random random = new java.util.Random(System.nanoTime());

    /**
     * Inject a random integer into static fields that are annotated with
     * {@code @Random} and can be assigned an integer value.
     */
    @Override
    public void beforeAll(ExtensionContext context) {
        Class<?> testClass = context.getRequiredTestClass();
        injectFields(testClass, null, ModifierSupport::isStatic);
    }

    /**
     * Inject a random integer into non-static fields that are annotated with
     * {@code @Random} and can be assigned an integer value.
     */
    @Override
    public void beforeEach(ExtensionContext context) {
        Class<?> testClass = context.getRequiredTestClass();
        Object testInstance = context.getRequiredTestInstance();
        injectFields(testClass, testInstance, ModifierSupport::isNotStatic);
    }

    /**
     * Determine if the parameter is annotated with {@code @Random} and can be
     * assigned an integer value.
     */
    @Override
    public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
        return pc.isAnnotated(Random.class) && isInteger(pc.getParameter().getType());
    }

    /**
     * Resolve a random integer.
     */
    @Override
    public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) {
        return this.random.nextInt();
    }

    private void injectFields(Class<?> testClass, Object testInstance,
            Predicate<Field> predicate) {

        predicate = predicate.and(field -> isInteger(field.getType()));
        findAnnotatedFields(testClass, Random.class, predicate)
            .forEach(field -> {
                try {
                    field.setAccessible(true);
                    field.set(testInstance, this.random.nextInt());
                }
                catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            });
    }

    private static boolean isInteger(Class<?> type) {
        return type == Integer.class || type == int.class;
    }

}
@ExtendWith字段的扩展注册顺序

通过 on 字段以声明方式注册的扩展将使用确定性但故意不明显的算法@ExtendWith相对于@RegisterExtension字段和其他字段进行排序。@ExtendWith但是,@ExtendWith可以使用@Order注释对字段进行排序。有关详细信息,请参阅字段的扩展注册顺序提示。@RegisterExtension

@ExtendWith字段可以是静态的static,也可以是非静态的。有关字段的静态字段实例字段的文档 也 @RegisterExtension适用于@ExtendWith字段。

5.2.2. 程序化扩展注册

开发人员可以通过@RegisterExtension.

当通过 声明性注册扩展时@ExtendWith,它通常只能通过注释进行配置。相反,当通过 注册扩展时,可以通过编程方式@RegisterExtension对其进行配置 - 例如,以便将参数传递给扩展的构造函数、静态工厂方法或构建器 API。

延期登记令

默认情况下,通过字段以编程方式@RegisterExtension或通过@ExtendWith字段以声明方式注册的扩展将使用确定性但故意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行扩展,从而允许可重复的构建。然而,有时需要以明确的顺序注册扩展。为此,请使用 注释@RegisterExtension字段或@ExtendWith字段@Order

任何@RegisterExtension字段或未@ExtendWith注释的字段都将使用值为 的默认@Order顺序进行排序。这允许带注释的扩展字段在未带注释的扩展字段之前或之后显式排序。显式顺序值小于默认顺序值的扩展将在未注释的扩展之前注册。同样,显式顺序值大于默认顺序值的扩展将在未注释的扩展之后注册。例如,相对于其他以编程方式注册的扩展,为扩展分配大于默认顺序值的显式顺序值允许最后注册回调扩展之前和首先注册回调扩展之后。Integer.MAX_VALUE / 2@Order

@RegisterExtension字段不得null(在评估时)但可以是静态的static或非静态的。
静态字段

如果@RegisterExtension字段是static,则扩展将在通过在类级别注册的扩展之后注册@ExtendWith。此类静态扩展不受其可以实现的扩展 API 的限制。因此,通过静态字段注册的扩展可以实现类级和实例级扩展 API,例如BeforeAllCallbackAfterAllCallbackTestInstancePostProcessorTestInstancePreDestroyCallback以及方法级扩展 API BeforeEachCallback,例如 等。

在以下示例中,server测试类中的字段是使用WebServerExtension. 配置的内容WebServerExtension将自动注册为类级别的扩展 - 例如,以便在类中的所有测试之前启动服务器,然后在类中的所有测试完成后停止服务器。此外,如果需要,用@BeforeAll或注解的静态生命周期方法@AfterAll以及@BeforeEach@AfterEach@Test方法可以通过该字段访问扩展的实例 。server

通过 Java 中的静态字段注册扩展
class WebServerDemo {

    @RegisterExtension
    static WebServerExtension server = WebServerExtension.builder()
        .enableSecurity(false)
        .build();

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

}
Kotlin 中的静态字段

Kotlin 编程语言没有字段的概念staticprivate static但是,可以指示编译器使用Kotlin 中的注释生成字段@JvmStatic 。如果您希望 Kotlin 编译器生成public static字段,则可以使用@JvmField注释来代替。

WebServerDemo以下示例是上一节中已移植到 Kotlin的版本。

在 Kotlin 中通过静态字段注册扩展
class KotlinWebServerDemo {

    companion object {
        @JvmStatic
        @RegisterExtension
        val server = WebServerExtension.builder()
            .enableSecurity(false)
            .build()
    }

    @Test
    fun getProductList() {
        // Use WebClient to connect to web server using serverUrl and verify response
        val webClient = WebClient()
        val serverUrl = server.serverUrl
        assertEquals(200, webClient.get("$serverUrl/products").responseStatus)
    }
}
实例字段

如果@RegisterExtension字段是非静态的(即实例字段),则扩展将在测试类实例化之后注册,并且在每个注册都有机会后处理 TestInstancePostProcessor测试实例之后(可能注入测试类的实例)用于注释字段的扩展名)。因此,如果此类实例扩展实现了类级或实例级扩展 API(例如BeforeAllCallbackAfterAllCallback或 ) TestInstancePostProcessor,则这些 API 将不会得到支持。默认情况下,实例扩展将在通过 ; 在方法级别注册的扩展之后@ExtendWith注册。但是,如果测试类配置了 语义,则实例扩展将在通过 . 在方法级别注册的扩展之前@TestInstance(Lifecycle.PER_CLASS)注册 。@ExtendWith

在以下示例中,docs测试类中的字段通过调用自定义方法lookUpDocsDir()并将结果提供给. 配置的 将自动注册为方法级别的扩展。此外,如有必要, 、、 和方法可以通过字段访问扩展的实例。forPath()DocumentationExtensionDocumentationExtension@BeforeEach@AfterEach@Testdocs

通过实例字段注册的扩展
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 文件的文件夹org.junit.jupiter.api.extension.Extension内 命名的文件中提供其完全限定类名来注册自定义扩展。/META-INF/services

启用自动扩展检测

自动检测是一项高级功能,因此默认情况下不启用。要启用它,请将junit.jupiter.extensions.autodetection.enabled 配置参数设置为 true。这可以作为 JVM 系统属性、作为传递到 的配置参数提供,或者通过 JUnit 平台配置文件提供(有关详细信息,请参阅配置参数)。LauncherDiscoveryRequestLauncher

例如,要启用扩展的自动检测,您可以使用以下系统属性启动 JVM。

-Djunit.jupiter.extensions.autodetection.enabled=true

当启用自动检测时,通过该ServiceLoader机制发现的扩展将被添加到扩展注册表中的 JUnit Jupiter 全局扩展之后(例如,支持TestInfoTestReporter等)。

5.2.4. 扩展继承

注册的扩展在具有自顶向下语义的测试类层次结构中继承。类似地,在类级别注册的扩展在方法级别继承。此外,对于给定的扩展上下文及其父上下文,特定的扩展实现只能注册一次。因此,任何注册重复扩展实现的尝试都将被忽略。

5.3. 条件测试执行

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

ExecutionCondition对每个容器(例如,测试类)评估an以确定是否应根据提供的 执行它包含的所有测试 ExtensionContext。类似地,对每个测试ExecutionCondition进行评估,以确定是否应根据提供的 执行给定的测试方法 ExtensionContext

当注册多个ExecutionCondition扩展时,一旦其中一个条件返回disabled ,容器或测试就会被禁用。因此,无法保证评估条件,因为另一个扩展可能已经导致容器或测试被禁用。换句话说,评估的工作方式类似于短路布尔 OR 运算符。

具体示例请参见DisabledCondition和的源代码。@Disabled

5.3.1. 停用条件

有时,在某些条件未激活的情况运行测试套件可能会很有用。例如,您可能希望运行测试,即使它们带有注释,@Disabled以便查看它们是否仍然损坏。为此,请为 junit.jupiter.conditions.deactivate 配置参数提供一个模式,以指定当前测试运行应停用(即不评估)哪些条件。该模式可以作为 JVM 系统属性、作为 传递给 的配置参数提供,或者通过 JUnit 平台配置文件提供(有关详细信息,请参阅配置参数)。LauncherDiscoveryRequestLauncher

例如,要停用 JUnit 的@Disabled条件,您可以使用以下系统属性启动 JVM。

-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition

模式匹配语法

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

5.4. 测试实例预构造回调

TestInstancePreConstructCallback定义希望在构建测试实例之前 调用Extensions的 API (通过构造函数调用或 via )。TestInstanceFactory

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

5.5. 测试实例工厂

TestInstanceFactoryExtensions为希望创建测试类实例的API 定义。

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

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

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

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

5.6. 测试实例后处理

TestInstancePostProcessorExtensions为希望发布流程测试实例的API 定义。

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

有关具体示例,请参阅MockitoExtension和 的 源代码SpringExtension

5.7. 测试实例预销毁回调

TestInstancePreDestroyCallback定义了希望在测试中使用测试实例之后和销毁测试实例之前Extensions处理测试实例的 API 。

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

5.8. 参数分辨率

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

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

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

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

ParameterContext因此,提供给实现的 API包括ParameterResolver以下用于正确查找参数注释的便捷方法。强烈建议扩展作者使用这些方法而不是 中提供的方法,java.lang.reflect.Parameter以避免 JDK 中出现此错误。

  • boolean isAnnotated(Class<? extends Annotation> annotationType)

  • Optional<A> findAnnotation(Class<A> annotationType)

  • List<A> findRepeatableAnnotations(Class<A> annotationType)

其他扩展还可以利用注册的ParameterResolvers方法和构造函数调用,ExecutableInvoker通过 .getExecutableInvoker()ExtensionContext

5.9. 测试结果处理

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

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

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

  • testAborted测试方法中止后调用

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

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

实现此接口的扩展可以在类级别、实例级别或方法级别注册。当在类级别注册时,将为任何包含的测试方法(包括类中的测试方法)TestWatcher调用a 。当在方法级别注册时,只会针对 其注册的测试方法调用 a。@NestedTestWatcher

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

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

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

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

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

5.10. 测试生命周期回调

以下接口定义了用于在测试执行生命周期的各个点扩展测试的 API。请参阅以下部分的示例以及包中每个接口的 Javadocorg.junit.jupiter.api.extension以了解更多详细信息。

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

5.10.1. 测试执行回调之前和之后

BeforeTestExecutionCallback并为希望添加将分别在执行测试方法之前和 之后立即执行的行为的AfterTestExecutionCallbackAPI 定义 。因此,这些回调非常适合计时、跟踪和类似的用例。如果您需要实现围绕和方法调用的回调,请实现 和。Extensions @BeforeEach@AfterEachBeforeEachCallbackAfterEachCallback

以下示例演示如何使用这些回调来计算和记录测试方法的执行时间。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注册了TimingExtensionvia @ExtendWith,因此其测试在执行时将应用此计时。

使用示例 TimingExtension 的测试类
@ExtendWith(TimingExtension.class)
class TimingExtensionTests {

    @Test
    void sleep20ms() throws Exception {
        Thread.sleep(20);
    }

    @Test
    void sleep50ms() throws Exception {
        Thread.sleep(50);
    }

}

TimingExtensionTests以下是运行时生成的日志记录的示例。

 INFO: Method [sleep20ms] took 24 ms.
INFO: Method [sleep50ms] took 53 ms.

5.11. 异常处理

在测试执行期间抛出的异常可以在进一步传播之前被拦截并进行相应处理,以便可以在专门的Extensions. JUnit Jupiter 为Extensions希望通过 处理@Test方法期间抛出的异常以及通过测试生命周期方法( 、和 )TestExecutionExceptionHandler 期间抛出的异常提供 API 。@BeforeAll@BeforeEach@AfterEach@AfterAllLifecycleMethodExecutionExceptionHandler

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

异常处理扩展,用于过滤测试执行中的 IOException
public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {

    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
            throws Throwable {

        if (throwable instanceof IOException) {
            return;
        }
        throw throwable;
    }
}

另一个示例展示了如何在设置和清理过程中抛出意外异常时准确记录被测应用程序的状态。请注意,与依赖生命周期回调(根据测试状态可能执行也可能不执行)不同,此解决方案保证在失败@BeforeAll@BeforeEach@AfterEach后立即执行@AfterAll

记录错误时应用程序状态的异常处理扩展
class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler {

    @Override
    public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during class setup");
        throw ex;
    }

    @Override
    public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during test setup");
        throw ex;
    }

    @Override
    public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during test cleanup");
        throw ex;
    }

    @Override
    public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during class cleanup");
        throw ex;
    }
}

可以按声明顺序为同一生命周期方法调用多个执行异常处理程序。如果其中一个处理程序吞下了已处理的异常,则后续处理程序将不会被执行,并且不会将任何故障传播到 JUnit 引擎,就好像该异常从未引发过一样。处理程序还可以选择重新抛出异常或抛出不同的异常,可能会包装原始异常。

扩展实现LifecycleMethodExecutionExceptionHandler希望处理在类级别上抛出的异常@BeforeAll@AfterAll需要在类级别上注册,而BeforeEach和 的处理程序AfterEach也可以为单个测试方法注册。

注册多个异常处理扩展
// 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. 拦截调用

InvocationInterceptorExtensions为希望拦截对测试代码的调用定义 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;
        }
    }
}

5.13。为测试模板提供调用上下文

一种@TestTemplate方法只有在至少 TestTemplateInvocationContextProvider注册了一个方法后才能执行。每个这样的提供者负责提供Stream实例TestTemplateInvocationContext。每个上下文可以指定一个自定义显示名称和一个仅用于该@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 ✔

扩展TestTemplateInvocationContextProviderAPI 主要用于实现不同类型的测试,这些测试依赖于在不同上下文中重复调用类似测试的方法 - 例如,使用不同的参数,通过不同的方式准备测试类实例,或者在不修改上下文的情况下多次调用。请参阅重复测试参数化测试的实现,它们使用此扩展点来提供其功能。

5.14。在扩展中保持状态

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

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

5.15。扩展中支持的实用程序

junit-platform-commons工件公开了一个名为的包 org.junit.platform.commons.support,其中包含用于处理注释、类、反射和类路径扫描任务的维护实用程序方法。TestEngine鼓励 Extension作者使用这些受支持的方法,以便与 JUnit 平台的行为保持一致。

5.15.1. 注释支持

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

5.15.2. 班级支持

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

5.15.3。反射支持

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

5.15.4。修改器支持

ModifierSupport提供用于使用成员和类修饰符的静态实用方法 — 例如,确定成员是否声明为publicprivateabstract、等。有关更多详细信息,static请参阅 Javadoc 。ModifierSupport

5.16。用户代码和扩展的相对执行顺序

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

另请参阅:测试执行顺序

5.16.1. 用户和分机代码

下图说明了用户提供的代码和扩展代码的相对顺序。用户提供的测试和生命周期方法以橙色显示,由扩展实现的回调代码以蓝色显示。灰色框表示执行单个测试方法,并且将对测试类中的每个测试方法重复执行。

扩展生命周期
用户代码和分机代码

下表进一步解释了 用户代码和分机代码图中的十六个步骤。

接口/注释 描述

1

界面org.junit.jupiter.api.extension.BeforeAllCallback

在执行容器的所有测试之前执行的扩展代码

2

注解org.junit.jupiter.api.BeforeAll

在执行容器的所有测试之前执行的用户代码

3

界面org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleBeforeAllMethodExecutionException

用于处理@BeforeAll方法抛出的异常的扩展代码

4

界面org.junit.jupiter.api.extension.BeforeEachCallback

在执行每个测试之前执行的扩展代码

5

注解org.junit.jupiter.api.BeforeEach

在执行每个测试之前执行的用户代码

6

界面org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleBeforeEachMethodExecutionException

用于处理@BeforeEach方法抛出的异常的扩展代码

7

界面org.junit.jupiter.api.extension.BeforeTestExecutionCallback

在执行测试之前立即执行扩展代码

8

注解org.junit.jupiter.api.Test

实际测试方法的用户代码

9

界面org.junit.jupiter.api.extension.TestExecutionExceptionHandler

用于处理测试期间抛出的异常的扩展代码

10

界面org.junit.jupiter.api.extension.AfterTestExecutionCallback

测试执行后立即执行的扩展代码及其相应的异常处理程序

11

注解org.junit.jupiter.api.AfterEach

每次测试执行后执行的用户代码

12

界面org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleAfterEachMethodExecutionException

用于处理@AfterEach方法抛出的异常的扩展代码

13

界面org.junit.jupiter.api.extension.AfterEachCallback

每次测试执行后执行的扩展代码

14

注解org.junit.jupiter.api.AfterAll

执行容器的所有测试后执行的用户代码

15

界面org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleAfterAllMethodExecutionException

用于处理@AfterAll方法抛出的异常的扩展代码

16

界面org.junit.jupiter.api.extension.AfterAllCallback

执行容器的所有测试后执行的扩展代码

在最简单的情况下,仅执行实际的测试方法(步骤 8);所有其他步骤都是可选的,具体取决于用户代码是否存在或对相应生命周期回调的扩展支持。有关各种生命周期回调的更多详细信息,请参阅每个注释和扩展的相应 Javadoc。

上表中用户代码方法的所有调用都可以通过实现来拦截InvocationInterceptor

5.16.2. 回调的包装行为

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

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

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

  • @BeforeAll只要方法未被隐藏重写取代(即,仅基于签名进行替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之前@BeforeAll执行。 @BeforeAll

    • 同样,@BeforeAll接口中声明的方法只要未被隐藏覆盖,就会被继承,并且接口中的方法将在实现该接口的类中的方法之前@BeforeAll执行。 @BeforeAll

  • @AfterAll只要方法未被隐藏重写取代(即,仅基于签名进行替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之后@AfterAll执行。 @AfterAll

    • 同样,@AfterAll接口中声明的方法只要未被隐藏覆盖,就会被继承,并且接口中的方法将在实现该接口的类中的方法之后@AfterAll执行。 @AfterAll

  • @BeforeEach只要方法未被 覆盖取代(即仅基于签名替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之前@BeforeEach执行。 @BeforeEach

    • 同样,@BeforeEach声明为接口默认方法的方法只要不被重写,就会被继承,并且默认方法将在实现该接口的类中的方法之前@BeforeEach执行 。 @BeforeEach

  • @AfterEach只要方法未被 覆盖取代(即仅基于签名替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之后@AfterEach执行。 @AfterEach

    • 同样,@AfterEach声明为接口默认方法的方法只要不被重写,就会被继承,并且默认方法将在实现该接口的类中的方法之后@AfterEach执行 。 @AfterEach

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

分机1
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;

import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

public class Extension1 implements BeforeEachCallback, AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        beforeEachCallback(this);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        afterEachCallback(this);
    }

}
扩展2
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;

import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

public class Extension2 implements BeforeEachCallback, AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        beforeEachCallback(this);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        afterEachCallback(this);
    }

}
抽象数据库测试
import static example.callbacks.Logger.afterAllMethod;
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;

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

/**
 * Abstract base class for tests that use the database.
 */
abstract class AbstractDatabaseTests {

    @BeforeAll
    static void createDatabase() {
        beforeAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".createDatabase()");
    }

    @BeforeEach
    void connectToDatabase() {
        beforeEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".connectToDatabase()");
    }

    @AfterEach
    void disconnectFromDatabase() {
        afterEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".disconnectFromDatabase()");
    }

    @AfterAll
    static void destroyDatabase() {
        afterAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".destroyDatabase()");
    }

}
数据库测试演示
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;

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

/**
 * Extension of {@link AbstractDatabaseTests} that inserts test data
 * into the database (after the database connection has been opened)
 * and deletes test data (before the database connection is closed).
 */
@ExtendWith({ Extension1.class, Extension2.class })
class DatabaseTestsDemo extends AbstractDatabaseTests {

    @BeforeAll
    static void beforeAll() {
        beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".beforeAll()");
    }

    @BeforeEach
    void insertTestDataIntoDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
    }

    @Test
    void testDatabaseFunctionality() {
        testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
    }

    @AfterEach
    void deleteTestDataFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
    }

    @AfterAll
    static void afterAll() {
        beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".afterAll()");
    }

}

DatabaseTestsDemo执行测试类时,会记录以下内容。

 @BeforeAll AbstractDatabaseTests.createDatabase()
@BeforeAll DatabaseTestsDemo.beforeAll()
  Extension1.beforeEach()
  Extension2.beforeEach()
    @BeforeEach AbstractDatabaseTests.connectToDatabase()
    @BeforeEach DatabaseTestsDemo.insertTestDataIntoDatabase()
      @Test DatabaseTestsDemo.testDatabaseFunctionality()
    @AfterEach DatabaseTestsDemo.deleteTestDataFromDatabase()
    @AfterEach AbstractDatabaseTests.disconnectFromDatabase()
  Extension2.afterEach()
  Extension1.afterEach()
@BeforeAll DatabaseTestsDemo.afterAll()
@AfterAll AbstractDatabaseTests.destroyDatabase()

下面的序列图有助于进一步阐明测试类执行JupiterTestEngine时实际发生的情况。DatabaseTestsDemo

扩展 数据库测试演示
数据库测试演示

JUnit Jupiter 不保证在单个测试类或测试接口中声明的多个生命周期方法的执行顺序。有时 JUnit Jupiter 可能会按字母顺序调用此类方法。然而,事实并非如此。该顺序类似于@Test单个测试类中方法的顺序。

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

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

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

  • 在数据库连接打开之前插入测试数据,导致无法连接数据库。

  • 删除测试数据之前关闭数据库连接,导致无法连接数据库。

破碎的生命周期方法配置演示
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/**
 * Example of "broken" lifecycle method configuration.
 *
 * <p>Test data is inserted before the database connection has been opened.
 *
 * <p>Database connection is closed before deleting test data.
 */
@ExtendWith({ Extension1.class, Extension2.class })
class BrokenLifecycleMethodConfigDemo {

    @BeforeEach
    void connectToDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".connectToDatabase()");
    }

    @BeforeEach
    void insertTestDataIntoDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
    }

    @Test
    void testDatabaseFunctionality() {
        testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
    }

    @AfterEach
    void deleteTestDataFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
    }

    @AfterEach
    void disconnectFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".disconnectFromDatabase()");
    }

}

BrokenLifecycleMethodConfigDemo执行测试类时,会记录以下内容。

 Extension1.beforeEach()
Extension2.beforeEach()
  @BeforeEach BrokenLifecycleMethodConfigDemo.insertTestDataIntoDatabase()
  @BeforeEach BrokenLifecycleMethodConfigDemo.connectToDatabase()
    @Test BrokenLifecycleMethodConfigDemo.testDatabaseFunctionality()
  @AfterEach BrokenLifecycleMethodConfigDemo.disconnectFromDatabase()
  @AfterEach BrokenLifecycleMethodConfigDemo.deleteTestDataFromDatabase()
Extension2.afterEach()
Extension1.afterEach()

下面的序列图有助于进一步阐明测试类执行JupiterTestEngine时实际发生的情况。BrokenLifecycleMethodConfigDemo

扩展 BrokenLifecycleMethodConfigDemo
破碎的生命周期方法配置演示

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

6. 高级主题

6.1. JUnit 平台报告

junit-platform-reporting工件包含TestExecutionListener生成两种类型的 XML 测试报告的实现: 传统报告开放测试报告

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

6.1.1. 旧版 XML 格式

LegacyXmlReportGeneratingListener为 .txt 文件中的每个根生成单独的 XML 报告 TestPlan。请注意,生成的 XML 格式与基于 JUnit 4 的测试报告的事实标准兼容,该标准因 Ant 构建系统而流行。

控制台启动器LegacyXmlReportGeneratingListener使用它。

6.1.2. 开放测试报告 XML 格式

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

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

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

启用/禁用写入报告。

junit.platform.reporting.output.dir=<path>

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

如果启用,侦听器会 junit-platform-events-<random-id>.xml在配置的输出目录中创建一个以每个测试运行命名的 XML 报告文件。

开放测试报告 CLI 工具 可用于从基于事件的格式转换为更易于阅读的分层格式。
摇篮

对于 Gradle,可以通过系统属性启用和配置编写 Open Test Reporting 兼容的 XML 报告。以下示例将其输出目录配置为 Gradle 用于其自己的 XML 报告的同一目录。ACommandLineArgumentProvider 用于保持任务可在​​不同机器之间重新定位,这在使用 Gradle 的构建缓存时非常重要。

Groovy DSL
dependencies {
    testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.1")
}
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)
}
科特林DSL
dependencies {
    testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.1")
}
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 Surefire/Failsafe,您可以启用开放测试报告输出并将生成的 XML 文件配置为写入 Surefire/Failsafe 用于其自己的 XML 报告的同一目录,如下所示:

<project>
    <!-- ... -->
    <dependencies>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-reporting</artifactId>
            <version>1.10.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
                <configuration>
                    <properties>
                        <configurationParameters>
                            junit.platform.reporting.open.xml.enabled = true
                            junit.platform.reporting.output.dir = target/surefire-reports
                        </configurationParameters>
                    </properties>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <!-- ... -->
</project>
控制台启动器

使用控制台启动器时,您可以通过以下方式设置配置参数来启用开放测试报告输出--config

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

6.2. JUnit 平台套件引擎

JUnit 平台支持使用 JUnit 平台的任何测试引擎对测试套件进行声明式定义和执行。

6.2.1. 设置

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

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

  • junit-platform-suite-engine测试运行时TestEngine范围内:声明性测试套件的 API实现

两个必需的依赖项都聚合在junit-platform-suite 工件中,可以在测试范围中声明它,而不是在junit-platform-suite-api和上声明显式依赖项junit-platform-suite-engine
传递依赖
  • junit-platform-suite-commons测试范围内

  • junit-platform-launcher测试范围内

  • junit-platform-engine测试范围内

  • junit-platform-commons测试范围内

  • opentest4j测试范围内

6.2.2. @Suite示例

通过用它注释一个类,@Suite将其标记为 JUnit 平台上的测试套件。如以下示例所示,选择器和过滤器注释可用于控制套件的内容。

import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;

@Suite
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
@IncludeClassNamePatterns(".*Tests")
class SuiteDemo {
}
附加配置选项
有许多配置选项可用于发现和过滤测试套件中的测试。请参阅包的 Javadocorg.junit.platform.suite.api以获取支持的注释的完整列表和更多详细信息。

6.3. JUnit 平台测试套件

junit-platform-testkit工件支持在 JUnit 平台上执行测试计划,然后验证预期结果。从 JUnit Platform 1.4 开始,这种支持仅限于单个的执行TestEngine(请参阅引擎测试套件)。

6.3.1. 发动机测试套件

org.junit.platform.testkit.engine包支持在 JUnit 平台上执行TestPlan给定的运行,然后通过流畅的 API 访问结果以验证预期结果。TestEngine该 API 的关键入口点是 EngineTestKit提供名为engine()和 的静态工厂方法execute()。建议您选择其中一种engine()变体,以便从用于构建LauncherDiscoveryRequest.

如果您更喜欢使用APILauncherDiscoveryRequestBuilderLauncher构建您的,则必须使用中的变体 LauncherDiscoveryRequest之一。 execute()EngineTestKit

后续示例中将使用以下使用 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。TestEngine或者,您可以通过向静态工厂方法提供一个实例来 测试您自己的方法EngineTestKit.engine(TestEngine)

6.3.2. 断言统计数据

测试套件最常见的功能之一是能够针对TestPlan. 以下测试演示了如何在 JUnit Jupiter 中断言容器测试TestEngine的统计信息。有关可用统计信息的详细信息,请参阅 的 Javadoc EventStatistics

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()的计数是因为和 类都被视为容器。 startedsucceeded2JupiterTestEngineExampleTestCase

6.3.3. 断言事件

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

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

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

有关哪些条件可用于针对事件的 AssertJ 断言的详细信息,请参阅EventConditions.

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 选择测试类skippedTest()中的方法ExampleTestCase
3 执行TestPlan.
4 按测试事件过滤。
5 将测试 保存Events到局部变量。
6 可以选择断言预期的统计数据。
7 断言记录的测试事件恰好包含一个名为 的被跳过的 测试skippedTest作为"for demonstration purposes"原因。

failingTest()如果您想验证中的方法 抛出的异常类型ExampleTestCase,可以按如下方式执行。

有关哪些条件可用于针对事件和执行结果的 AssertJ 断言的详细信息,请分别查阅EventConditions和 的 Javadoc TestExecutionResultConditions

import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;

import example.ExampleTestCase;

import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;

class EngineTestKitFailedMethodDemo {

    @Test
    void verifyJupiterMethodFailed() {
        EngineTestKit.engine("junit-jupiter") (1)
            .selectors(selectClass(ExampleTestCase.class)) (2)
            .execute() (3)
            .testEvents() (4)
            .assertThatEvents().haveExactly(1, (5)
                event(test("failingTest"),
                    finishedWithFailure(
                        instanceOf(ArithmeticException.class), message("/ by zero"))));
    }

}
1 选择 JUnit Jupiter TestEngine
2 选择ExampleTestCase测试类别。
3 执行TestPlan.
4 按测试事件过滤。
5 断言记录的测试事件恰好包含一个失败的测试,该测试的名称 failingTest为类型为异常ArithmeticException且错误消息等于"/ by zero"

虽然通常是不必要的,但有时您需要验证TestPlan. 下面的测试演示了如何通过APIassertEventsMatchExactly()中的方法来实现这一点EngineTestKit

由于assertEventsMatchExactly()完全按照事件触发的顺序匹配条件,因此ExampleTestCase已用 进行注释@TestMethodOrder(OrderAnnotation.class),并且每个测试方法均已用 进行注释@Order(…​)。这使我们能够强制执行测试方法的顺序,从而使我们的verifyAllJupiterEvents()测试变得可靠。

如果您想在没有排序要求的情况下进行部分匹配,您可以分别使用方法和。assertEventsMatchLooselyInOrder()assertEventsMatchLoosely()

import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.container;
import static org.junit.platform.testkit.engine.EventConditions.engine;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.started;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;

import java.io.StringWriter;
import java.io.Writer;

import example.ExampleTestCase;

import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.opentest4j.TestAbortedException;

class EngineTestKitAllEventsDemo {

    @Test
    void verifyAllJupiterEvents() {
        Writer writer = // create a java.io.Writer for debug output

        EngineTestKit.engine("junit-jupiter") (1)
            .selectors(selectClass(ExampleTestCase.class)) (2)
            .execute() (3)
            .allEvents() (4)
            .debug(writer) (5)
            .assertEventsMatchExactly( (6)
                event(engine(), started()),
                event(container(ExampleTestCase.class), started()),
                event(test("skippedTest"), skippedWithReason("for demonstration purposes")),
                event(test("succeedingTest"), started()),
                event(test("succeedingTest"), finishedSuccessfully()),
                event(test("abortedTest"), started()),
                event(test("abortedTest"),
                    abortedWithReason(instanceOf(TestAbortedException.class),
                        message(m -> m.contains("abc does not contain Z")))),
                event(test("failingTest"), started()),
                event(test("failingTest"), finishedWithFailure(
                    instanceOf(ArithmeticException.class), message("/ by zero"))),
                event(container(ExampleTestCase.class), finishedSuccessfully()),
                event(engine(), finishedSuccessfully()));
    }

}
1 选择 JUnit Jupiter TestEngine
2 选择ExampleTestCase测试类别。
3 执行TestPlan.
4 按所有事件过滤。
5 将所有事件打印到所提供的writer用于调试目的。调试信息也可以写入OutputStream诸如System.out或 之类的文件中System.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 平台启动器 API

JUnit 5 的突出目标之一是使 JUnit 与其编程客户端(构建工具和 IDE)之间的接口更加强大和稳定。目的是将发现和执行测试的内部结构与外部所需的所有过滤和配置分离。

LauncherJUnit 5 引入了可用于发现、过滤和执行测试的概念。此外,第三方测试库(例如 Spock、Cucumber 和 FitNesse)可以通过提供自定义 TestEngine插入 JUnit 平台的启动基础架构中。

启动器 API 位于junit-platform-launcher模块中。

ConsoleLauncher项目中的 启动器 API 的一个示例使用者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 ,可用于调用特定的测试或一组测试。

客户可以通过注册一个或多个LauncherDiscoveryListener实现来 LauncherDiscoveryRequestBuilder深入了解测试发现期间发生的事件。默认情况下,构建器会注册一个“失败时中止”侦听器,该侦听器会在遇到第一次发现失败后中止测试发现。可以通过配置参数LauncherDiscoveryListener更改 默认值 。junit.platform.discovery.listener.default

6.4.2. 执行测试

要执行测试,客户端可以使用LauncherDiscoveryRequest与发现阶段相同的测试或创建新请求。可以通过使用以下示例中的注册一个或多个TestExecutionListener实现来实现测试进度和报告。Launcher

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(),但您可以使用 a TestExecutionListener来聚合结果。例如,请参见 SummaryGeneratingListenerLegacyXmlReportGeneratingListenerUniqueIdTrackingListener

所有TestExecutionListener方法均按顺序调用。开始事件的方法按注册顺序调用,而完成事件的方法按相反顺序调用。executionStarted在所有调用返回 之前,测试用例执行不会开始。

6.4.3. 注册测试引擎

6.4.4. 注册 PostDiscoveryFilter

除了指定发现后过滤器作为LauncherDiscoveryRequest 传递给LauncherAPI 的一部分之外,PostDiscoveryFilter还将在运行时通过 Java 机制发现实现ServiceLoader,并自动应用 Launcher到那些属于请求的过滤器中。

例如,在文件中example.CustomTagFilter实现PostDiscoveryFilter和声明的类/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter 会自动加载和应用。

6.4.5。注册 LauncherSessionListener

LauncherSessionListener当 a 打开(在第一个发现并执行测试之前)和关闭(当不再发现或执行测试时)时,已注册的实现会 收到LauncherSession通知Launcher。它们可以通过LauncherConfig传递给 的来以编程方式注册LauncherFactory,或者可以在运行时通过 Java 的机制发现它们ServiceLoader并自动注册LauncherSession(除非自动注册被禁用。)

工具支持

已知以下构建工具和 IDE 可以提供全面支持LauncherSession

  • Gradle 4.6 及更高版本

  • Maven Surefire/Failsafe 3.0.0-M6 及更高版本

  • IntelliJ IDEA 2017.3 及更高版本

其他工具也可能有效,但尚未经过明确测试。

用法示例

ALauncherSessionListener非常适合实现每个 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

为了拦截Launcher和 实例的创建LauncherSessionListener以及对前者的discover和方法的调用,客户端可以通过 Java 机制通过另外将 配置参数设置为 来注册自定义实现。executeLauncherInterceptorServiceLoaderjunit.platform.launcher.interceptors.enabled true

一个典型的用例是创建一个自定义的替换ClassLoaderJUnit 平台用来加载测试类和引擎实现的方法。

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或通过 API 以编程方式注册它们之外LauncherLauncherDiscoveryListener还可以在运行时通过 Java ServiceLoader机制发现自定义实现,并Launcher通过LauncherFactory.

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

6.4.8。注册一个 TestExecutionListener

除了用于Launcher以编程方式注册测试执行侦听器的公共 API 方法之外,TestExecutionListener还将在运行时通过 JavaServiceLoader机制发现自定义实现,并 Launcher通过LauncherFactory.

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

6.4.9。配置 TestExecutionListener

当 aTestExecutionListener通过 API 以编程方式注册时Launcher,侦听器可以提供对其进行配置的编程方式 - 例如,通过其构造函数、setter 方法等。但是,当 a 通过TestExecutionListenerJavaServiceLoader机制自动注册时(请参阅 注册 TestExecutionListener),用户无法直接配置监听器。在这种情况下,a 的作者可以选择通过配置参数TestExecutionListener来配置侦听器。然后,侦听器可以通过提供的 和回调方法访问配置参数。请参阅的示例。TestPlantestPlanExecutionStarted(TestPlan)testPlanExecutionFinished(TestPlan)UniqueIdTrackingListener

6.4.10. 停用 TestExecutionListener

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

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

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

模式匹配语法

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

6.4.11. 配置启动器

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

通过LauncherAPI 运行测试时,您可以通过将配置参数设置 为 来启用试运行模式。在此模式下,实际上不会执行任何测试,但会通知注册实例,就好像所有测试已被跳过并且其容器已成功一样。这对于测试构建配置中的更改或验证侦听器是否按预期调用而无需等待所有测试执行非常有用。junit.platform.execution.dryRun.enabled trueLauncherTestExecutionListener

6.5. 测试引擎

ATestEngine有助于发现执行特定编程模型的测试。

例如,JUnit 提供了一个TestEngine发现并执行使用 JUnit Jupiter 编程模型编写的测试的方法(请参阅编写测试扩展模型)。

6.5.1. JUnit 测试引擎

JUnit 提供了三种TestEngine实现。

6.5.2. 定制测试引擎

您可以通过实现junit-platform-engineTestEngine模块中的接口 并注册您的引擎来贡献您自己的自定义。

每个都TestEngine必须提供自己唯一的 ID,从 中 发现EngineDiscoveryRequest测试,并根据执行ExecutionRequest这些测试。

唯一的 ID前缀junit-是为 JUnit 团队的 TestEngine 保留的

JUnit 平台Launcher强制只有TestEngineJUnit 团队发布的实现才可以使用junit-TestEngineID 的前缀。

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

  • 如果任何第三方TestEngine使用该junit-前缀作为其 ID,则会记录一条警告消息。JUnit 平台的后续版本将针对此类违规引发异常。

为了在启动 JUnit 平台之前促进 IDE 和工具中的测试发现,TestEngine鼓励实现使用注释@Testable 。例如, JUnit Jupiter 中的@Test@TestFactory注释使用 进行元注释@Testable@Testable有关更多详细信息,请参阅 Javadoc 。

如果您的自定义TestEngine需要配置,请考虑允许用户通过配置参数提供配置。但请注意,强烈建议您对测试引擎支持的所有配置参数使用唯一的前缀。这样做将确保您的配置参数名称与其他测试引擎的配置参数名称之间不存在冲突。此外,由于配置参数可以作为 JVM 系统属性提供,因此明智的做法是避免与其他系统属性的名称发生冲突。例如,JUnit Jupiter 使用junit.jupiter.作为其所有支持的配置参数的前缀。此外,与上面有关IDjunit-前缀的 警告一样TestEngine,您不应将junit.其用作您自己的配置参数名称的前缀。

虽然目前还没有关于如何实现自定义的官方指南,但您可以参考JUnit 测试引擎TestEngine的实现或JUnit 5 wiki中列出的第三方测试引擎的实现 。您还可以在 Internet 上找到各种教程和博客,演示如何编写自定义.TestEngine

HierarchicalTestEngine是 SPI 的一个方便的抽象基础实现 TestEngine(由junit-jupiter-engine),只需要实现者提供测试发现的逻辑。TestDescriptors它实现了接口的执行Node,包括对并行执行的支持。

6.5.3. 注册测试引擎

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

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

6.5.4. 要求

本节中的“必须”、“不得”、“必需”、“应”、“不应”、“应该”、“不应该”、“建议”、“可以”和“可选”等词语是为了按照 RFC 2119 中的描述进行解释
强制性要求

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

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

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

  • 传递给的executionSkippedexecutionStartedexecutionFinished方法 必须为 树中返回的每个节点最多调用一次。父节点必须报告为在其子节点之前开始,并在其子节点之后完成。如果节点被报告为已跳过,则不得为其后代报告任何事件。EngineExecutionListenerTestEngine.execute() TestDescriptorTestEngine.discover()

增强兼容性

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

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

  • 解析 时UniqueIdSelectors, aTestEngine 仅返回TestDescriptor 具有匹配的唯一 ID 的实例(包括其祖先),但可能会返回执行所选测试所需的其他同级节点或其他节点。

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

7. API 演变

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

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

7.1. API版本和状态

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

地位 描述

INTERNAL

除 JUnit 本身之外,不得由任何代码使用。可能会被删除,恕不另行通知。

DEPRECATED

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

EXPERIMENTAL

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

MAINTAINED

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

STABLE

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

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

7.2. 实验性 API

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

包裹名字 类型名称 自从

org.junit.jupiter.api

Timeout.ThreadMode (枚举)

5.9

org.junit.jupiter.api.extension

AnnotatedElementContext (界面)

5.10

org.junit.jupiter.api.extension

DynamicTestInvocationContext (界面)

5.8

org.junit.jupiter.api.extension

ExecutableInvoker (界面)

5.9

org.junit.jupiter.api.extension

TestInstancePreConstructCallback (界面)

5.9

org.junit.jupiter.api.io

CleanupMode (枚举)

5.9

org.junit.jupiter.api.io

TempDirFactory (界面)

5.10

org.junit.jupiter.params.converter

AnnotationBasedArgumentConverter (班级)

5.10

org.junit.jupiter.params.provider

AnnotationBasedArgumentsProvider (班级)

5.10

org.junit.platform.engine.discovery

IterationSelector (班级)

1.9

org.junit.platform.engine.support.store

NamespacedHierarchicalStore (班级)

5.10

org.junit.platform.engine.support.store

NamespacedHierarchicalStoreException (班级)

5.10

org.junit.platform.jfr

FlightRecordingDiscoveryListener (班级)

1.8

org.junit.platform.jfr

FlightRecordingExecutionListener (班级)

1.8

org.junit.platform.launcher

LauncherDiscoveryListener (界面)

1.6

org.junit.platform.launcher

LauncherInterceptor (界面)

1.10

org.junit.platform.launcher

TestPlan.Visitor (界面)

1.10

org.junit.platform.launcher.listeners

UniqueIdTrackingListener (班级)

1.8

org.junit.platform.reporting.open.xml

OpenTestReportGeneratingListener (班级)

1.9

org.junit.platform.suite.api

SelectMethod (注解)

1.10

org.junit.platform.suite.api

SelectMethods (注解)

1.10

7.3. 已弃用的 API

下表列出了当前通过 指定为已弃用的@API(status = DEPRECATED)API 。您应该尽可能避免使用已弃用的 API,因为此类 API 可能会在即将发布的版本中被删除。

包裹名字 类型名称 自从

org.junit.jupiter.api

MethodOrderer.Alphanumeric (班级)

5.7

org.junit.platform.commons.util

BlacklistedExceptions (班级)

1.7

org.junit.platform.commons.util

PreconditionViolationException (班级)

1.5

org.junit.platform.engine.support.filter

ClasspathScanningSupport (班级)

1.5

org.junit.platform.engine.support.hierarchical

SingleTestExecutor (班级)

1.2

org.junit.platform.launcher.listeners

LegacyReportingUtils (班级)

1.6

org.junit.platform.runner

JUnitPlatform (班级)

1.8

org.junit.platform.suite.api

UseTechnicalNames (注解)

1.8

7.4. @API工具支持

@API Guardian项目计划为使用@API注释的 API 的发布者和消费者提供工具支持。例如,工具支持可能会提供一种方法来检查是否按照@API注释声明使用 JUnit API。

8. 贡献者

直接在 GitHub 上浏览当前的贡献者列表

9. 发行说明

发行说明可在此处获取。

10. 附录

10.1. 可重复的构建

从版本 5.7 开始,JUnit 5 的目标是使其非 javadoc JAR 具有可 重现性

在相同的构建条件下(例如 Java 版本),重复构建应提供相同的逐字节输出。

这意味着任何人都可以在 Maven Central/Sonatype 上重现工件的构建条件,并在本地生成相同的输出工件,从而确认存储库中的工件实际上是从该源代码生成的。

10.2. 依赖元数据

最终版本和里程碑的工件部署到Maven Central ,快照工件部署到/org/junit下的 Sonatype快照存储库

10.2.1. JUnit平台

  • 群组IDorg.junit.platform

  • 版本1.10.1

  • 工件 ID

    junit-platform-commons

    JUnit 平台的通用 API 和支持实用程序。任何带有注释的 API @API(status = INTERNAL)仅供在 JUnit 框架本身内使用。不支持外部方对内部 API 的任何使用!

    junit-platform-console

    支持从控制台在 JUnit 平台上发现和执行测试。有关详细信息,请参阅控制台启动器

    junit-platform-console-standalone

    Maven Central 中的junit-platform-console-standalone目录下提供了包含所有依赖项的可执行 JAR 。有关详细信息,请参阅控制台启动器

    junit-platform-engine

    用于测试引擎的公共 API。有关详细信息,请参阅注册测试引擎。

    junit-platform-jfr

    为 JUnit 平台上的 Java Flight Recorder 事件 提供LauncherDiscoveryListener和。TestExecutionListener有关详细信息,请参阅飞行记录器支持

    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 Platform Suite 工件,可在 Gradle 和 Maven 等构建工具中 传递依赖项 junit-platform-suite-api并简化依赖项管理。junit-platform-suite-engine

    junit-platform-suite-api

    用于在 JUnit 平台上配置测试套件的注释。由 JUnit Platform Suite EngineJUnitPlatform runner支持。

    junit-platform-suite-commons

    用于在 JUnit 平台上执行测试套件的通用支持实用程序。

    junit-platform-suite-engine

    在 JUnit 平台上执行测试套件的引擎;仅在运行时需要。有关详细信息,请参阅 JUnit 平台套件引擎

    junit-platform-testkit

    支持执行给定的测试计划TestEngine,然后通过流畅的 API 访问结果以验证预期结果。

10.2.2. JUnit木星

  • 群组IDorg.junit.jupiter

  • 版本5.10.1

  • 工件 ID

    junit-jupiter

    JUnit Jupiter 聚合器工件可传递性地引入对 junit-jupiter-apijunit-jupiter-params和 的依赖关系,junit-jupiter-engine以简化 Gradle 和 Maven 等构建工具中的依赖关系管理。

    junit-jupiter-api

    用于编写测试扩展的JUnit Jupiter API 。

    junit-jupiter-engine

    JUnit Jupiter 测试引擎实现;仅在运行时需要。

    junit-jupiter-params

    支持JUnit Jupiter 中的参数化测试。

    junit-jupiter-migrationsupport

    支持从 JUnit 4 迁移到 JUnit Jupiter;仅需要支持 JUnit 4 的@Ignore注释和运行选定的 JUnit 4 规则。

10.2.3。JUnit 复古

  • 群组IDorg.junit.vintage

  • 版本5.10.1

  • 工件 ID

    junit-vintage-engine

    JUnit Vintage 测试引擎实现,允许在 JUnit 平台上运行老式JUnit 测试。老式测试包括使用 JUnit 3 或 JUnit 4 API 编写的测试或使用基于这些 API 构建的测试框架编写的测试。

10.2.4。物料清单 (BOM)

在使用MavenGradle引用多个上述工件时,以下 Maven 坐标下提供的物料清单POM可用于简化依赖关系管理 。

  • 群组IDorg.junit

  • 工件 IDjunit-bom

  • 版本5.10.1

10.2.5。依赖关系

上述大多数工件在其发布的 Maven POM 中都依赖于以下@API Guardian JAR。

  • 群组IDorg.apiguardian

  • 工件 IDapiguardian-api

  • 版本1.1.2

此外,上述大多数工件都直接或传递依赖于以下OpenTest4J JAR。

  • 群组IDorg.opentest4j

  • 工件 IDopentest4j

  • 版本1.3.0

10.3. 依赖关系图

组件图