1. 概述

本文档的目标是为那些编写测试的程序员、扩展开发人员(extension authors)和引擎开发人员(engine authors)以及构建工具和IDE供应商提供综合全面的参考。

1.1. JUnit 5 是什么?

JUnit 5跟以前的JUnit版本不一样,它由几大不同的模块组成,这些模块分别来自三个不同的子项目。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform是在JVM上 启动测试框架 的基础平台。它还定义了 TestEngine API,该API可用于开发在平台上运行的测试框架。此外,平台还提供了一个从命令行或者 GradleMaven 插件来启动的 控制台启动器 ,它就好比一个 基于JUnit 4的Runner 在平台上运行任何TestEngine

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

JUnit Vintage 提供了一个TestEngine,用于在平台上运行基于JUnit 3和JUnit 4的测试。

1.2. 支持的Java版本

JUnit 5需要Java 8(或更高)的运行时环境。不过,你仍然可以测试那些由老版本JDK编译的代码。

1.3. 获取帮助

与JUnit 5相关问题,可以在 Stack Overflow 进行提问,或者在 Gitter 上跟我们交流。


2. 安装

最终版本和里程碑版本已经被部署到Maven仓库中心。

快照版本被部署到 Sonatype 仓库 中的 /org/junit目录下。

2.1. 依赖元数据

2.1.1. JUnit Platform

  • Group ID: org.junit.platform

  • Version: 1.2.0

  • Artifact IDs:

junit-platform-commons

JUnit 内部通用类库/实用工具,它们仅用于JUnit框架本身,不支持任何外部使用,外部使用风险自负。

junit-platform-console

支持从控制台中发现和执行JUnit Platform上的测试。详情请参阅 控制台启动器

junit-platform-console-standalone

一个包含了Maven仓库中的 junit-platform-console-standalone 目录下所有依赖项的可执行JAR包。详情请参阅 控制台启动器

junit-platform-engine

测试引擎的公共API。详情请参阅 插入你自己的测试引擎

junit-platform-gradle-plugin

支持使用 Gralde 来发现和执行JUnit Platform上的测试。

junit-platform-launcher

配置和加载测试计划的公共API – 典型的使用场景是IDE和构建工具。详情请参阅 JUnit Platform启动器API

junit-platform-runner

在一个JUnit 4环境中的JUnit Platform上执行测试和测试套件的运行器。详情请参阅 使用JUnit 4运行JUnit Platform

junit-platform-suite-api

在JUnit Platform上配置测试套件的注解。被 JUnit Platform运行器 所支持,也有可能被第三方的TestEngine实现所支持。

junit-platform-surefire-provider

支持使用 Maven Surefire 来发现和执行JUnit Platform上的测试。

2.1.2. JUnit Jupiter

  • Group ID: org.junit.jupiter

  • Version: 5.3.0

  • Artifact IDs:

junit-jupiter-api

编写测试扩展 的JUnit Jupiter API。

junit-jupiter-engine

JUnit Jupiter测试引擎的实现,仅仅在运行时需要。

junit-jupiter-params

支持JUnit Jupiter中的 参数化测试

junit-jupiter-migration-support

支持从JUnit 4迁移到JUnit Jupiter,仅在使用了JUnit 4规则的测试中才需要。

2.1.3. JUnit Vintage

  • Group ID: org.junit.vintage

  • Version: 5.3.0

  • Artifact ID:

junit-vintage-engine

JUnit Vintage测试引擎实现,允许在新的JUnit Platform上运行低版本的JUnit测试,即那些以JUnit 3或JUnit 4风格编写的测试。

2.1.4. 物料清单 (BOM)

当使用 MavenGradle 引用多个上述artifacts时,可以使用下面Maven坐标下提供的物料清单POM 来缓解依赖管理。

  • Group ID: org.junit

  • Artifact ID: junit-bom

  • Version: 5.3.0

2.1.5. 依赖

以上所有artifacts在它们已发布的Maven POM中都依赖了下面的@API Guardian JAR文件。

  • Group ID: org.apiguardian

  • Artifact ID: apiguardian-api

  • Version: 1.0.0

此外,上面大部分artifacts都对下面的OpenTest4J JAR文件有直接或传递的依赖关系。

  • Group ID: org.opentest4j

  • Artifact ID: opentest4j

  • Version: 1.0.0

2.2. 依赖关系图

2.3 JUnit Jupiter示例工程

junit5-samples 代码库中包含了一系列基于JUnit Jupiter和JUnit Vintage的示例工程。你可以在下面的项目中找到相应的各自的构建脚本(例如:build.gradlepom.xml文件)


3. 编写测试

第一个测试用例

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

import org.junit.jupiter.api.Test;

class FirstJUnit5Tests {

    @Test
    void myFirstTest() {
        assertEquals(2, 1 + 1);
    }

}

3.1. 注解

JUnit Jupiter支持使用下面表格中的注解来配置测试和扩展框架。

所有的核心注解都位于junit-jupiter-api模块的 org.junit.jupiter.api 包中。

注解描述
@Test表示该方法是一个测试方法。与JUnit 4的@Test注解不同的是,它没有声明任何属性,因为JUnit Jupiter中的测试扩展是基于它们自己的专用注解来完成的。这样的方法会被继承,除非它们被覆盖
@ParameterizedTest表示该方法是一个 参数化测试。这样的方法会被继承,除非它们被覆盖
@RepeatedTest表示该方法是一个 重复测试 的测试模板。这样的方法会被继承,除非它们被覆盖
@TestFactory表示该方法是一个 动态测试 的测试工厂。这样的方法会被继承,除非它们被覆盖
@TestInstance用于配置所标注的测试类的 测试实例生命周期。这些注解会被继承
@TestTemplate表示该方法是一个 测试模板,它会依据注册的 提供者 所返回的调用上下文的数量被多次调用。 这样的方法会被继承,除非它们被覆盖
@DisplayName为测试类或测试方法声明一个自定义的显示名称。该注解不能被继承
@BeforeEach表示使用了该注解的方法应该在当前类中每一个使用了@Test@RepeatedTest@ParameterizedTest或者@TestFactory注解的方法之前 执行;类似于JUnit 4的 @Before。这样的方法会被继承,除非它们被覆盖
@AfterEach表示使用了该注解的方法应该在当前类中每一个使用了@Test@RepeatedTest@ParameterizedTest或者@TestFactory注解的方法之后 执行;类似于JUnit 4的 @After。这样的方法会被继承,除非它们被覆盖
@BeforeAll表示使用了该注解的方法应该在当前类中所有使用了@Test@RepeatedTest@ParameterizedTest或者@TestFactory注解的方法之前 执行;类似于JUnit 4的 @BeforeClass。这样的方法会被继承(除非它们被隐藏覆盖),并且它必须是 static方法(除非"per-class" 测试实例生命周期 被使用)。
@AfterAll表示使用了该注解的方法应该在当前类中所有使用了@Test@RepeatedTest@ParameterizedTest或者@TestFactory注解的方法之后执行;类似于JUnit 4的 @AfterClass。这样的方法会被继承(除非它们被隐藏覆盖),并且它必须是 static方法(除非"per-class" 测试实例生命周期 被使用)。
@Nested表示使用了该注解的类是一个内嵌、非静态的测试类。@BeforeAll@AfterAll方法不能直接在@Nested测试类中使用,(除非"per-class" 测试实例生命周期 被使用)。该注解不能被继承
@Tag用于声明过滤测试的tags,该注解可以用在方法或类上;类似于TesgNG的测试组或JUnit 4的分类。该注解能被继承,但仅限于类级别,而非方法级别。
@Disable用于禁用一个测试类或测试方法;类似于JUnit 4的@Ignore。该注解不能被继承。
@ExtendWith用于注册自定义 扩展。该注解不能被继承

@Test@TestTemplate@RepeatedTest@BeforeAll@AfterAll@BeforeEach@AfterEach 注解标注的方法不可以有返回值。

⚠️ 某些注解目前可能还处于试验阶段。详细信息请参阅 试验性API 中的表格。

3.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 {
}

3.2. 测试类和测试方法

任何使用元注解@Test@RepeatedTest@ParameterizedTest@ TestFactory@TestTemplate标注的实例方法都是一个测试方法。任何包含至少一种测试方法的顶级类或静态成员类都是一个测试类

任何一个测试方法

一个标准的测试类

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

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
    }

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }

}

📒 不必将测试类和测试方法声明为public

3.3. 显示名称

测试类和测试方法可以声明自定义的显示名称 – 空格、特殊字符甚至是emojis表情 – 都可以显示在测试运行器和测试报告中。

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

}

3.4. 断言

JUnit Jupiter附带了很多JUnit 4就已经存在的断言方法,并增加了一些适合与Java8 Lambda一起使用的断言。所有的JUnit Jupiter断言都是 org.junit.jupiter.api.Assertions 类中static方法。

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

import org.junit.jupiter.api.Test;

class AssertionsDemo {

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "The optional assertion message is now the last parameter.");
        assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and any
        // failures will be reported together.
        assertAll("person",
            () -> assertEquals("John", 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("n"))
                );
            },
            () -> {
                // 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() {
        Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", 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.
            Thread.sleep(100);
        });
    }

    private static String greeting() {
        return "hello world!";
    }

}

3.4.1. 第三方断言类库

虽然JUnit Jupiter提供的断言工具包已经满足了许多测试场景,但有时我们会遇到需要更加强大且具备例如匹配器 功能的场景。在这些场景中,JUnit团队推荐使用第三方断言类库,例如:AssertJHamcrestTruth 等等。因此,开发人员可以自由使用他们选择的断言类库。

举个例子,匹配器 和流式调用的API组合起来使用可以让断言更加具有描述性和可读性。然而,JUnit Jupiter的 org.junit.jupiter.api.Assertions 类没有提供一个类似于JUnit 4的org.junit.Assert类中 assertThat() 方法,该方法接受一个Hamcrest Matcher。所以,我们鼓励开发人员使用由第三方断言库提供的匹配器的内置支持。

下面的例子演示如何在JUnit Jupiter中使用Hamcrest提供的assertThat()。只要Hamcrest库已经被添加到classpath中,你就可以静态导入诸如assertThat()is()以及equalTo()方法,然后在测试方法中使用它们,如下面代码所示的assertWithHamcrestMatcher()方法。

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

import org.junit.jupiter.api.Test;

class HamcrestAssertionDemo {

    @Test
    void assertWithHamcrestMatcher() {
        assertThat(2 + 1, is(equalTo(3)));
    }

}

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

3.5. 假设

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 org.junit.jupiter.api.Test;

class AssumptionsDemo {

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

        // perform these assertions in all environments
        assertEquals("a string", "a string");
    }

}

3.6. 禁用测试

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

下面是一个 @Disable 的测试用例。

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

@Disabled
class DisabledClassDemo {
    @Test
    void testWillBeSkipped() {
    }
}

下面是一个包含@Disable测试方法的测试类。

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

class DisabledTestsDemo {

    @Disabled
    @Test
    void testWillBeSkipped() {
    }

    @Test
    void testWillBeExecuted() {
    }
}

3.7. 条件测试执行

JUnit Jupiter中的 ExecutionCondition 扩展API允许开发人员以编程的方式基于某些条件启用或禁用容器或测试。这种情况的最简单示例是内置的 DisabledCondition,它支持 @Disabled 注解(请参阅 禁用测试)。除了@Disabled之外,JUnit Jupiter还支持 org.junit.jupiter.api.condition包中的其他几个基于注解的条件,允许开发人员以 声明 的方式启用或禁用容器和测试。详情请参阅一下章节。

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

⚠️ 以下各节中列出的每个条件注解只能在给定的测试接口,测试类或测试方法上声明一次。如果条件注解在给定元素上直接存在,间接存在或元存在多次,则仅使用由JUnit发现的第一个此类注解;任何其他声明都将被默默忽略。但是请注意,每个条件注解可以与org.junit.jupiter.api.condition包中的其他条件一起使用。

3.7.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 {
}

3.7.2 Java运行时环境条件

可以通过 @EnabledOnJre@DisabledOnJre 注解在特定版本的Java运行时环境(JRE)上启用或禁用容器或测试。

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

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

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

3.7.3. 系统属性条件

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

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

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

3.7.4. 环境变量条件

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

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

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

3.7.5 基于脚本的条件

根据对通过 @EnabledIf 或 [@DisabledIf](https://junit.org/junit5/docs/5.3.0/api/org/junit/jupiter/api/condition/DisabledIf.html 注解配置的脚本的评估,JUnit Jupiter提供了 启用或禁用 容器或测试的功能。脚本可以用JavaScript,Groovy或任何其他支持Java脚本API的脚本语言编写,由JSR 223定义。

⚠️ 通过 @EnabledIf 或 [@DisabledIf](https://junit.org/junit5/docs/5.3.0/api/org/junit/jupiter/api/condition/DisabledIf.html 执行条件测试目前是一项试验性功能。有关详细信息,请参阅 实验性API 中的表格。

💡 如果脚本的逻辑仅依赖于当前的操作系统,当前的Java运行时环境版本,特定的JVM系统属性或特定的环境变量,则应考虑使用专用于此目的的内置注释之一。有关更多详细信息,请参阅本章的前几节。

📒 如果你发现自己多次使用基于脚本的相同条件,请考虑编写一个专用的 ExecutionCondition 扩展,以便以更快,更安全,更易维护的方式实现条件。

@Test // Static JavaScript expression.
@EnabledIf("2 * 3 == 6")
void willBeExecuted() {
    // ...
}

@RepeatedTest(10) // Dynamic JavaScript expression.
@DisabledIf("Math.random() < 0.314159")
void mightNotBeExecuted() {
    // ...
}

@Test // Regular expression testing bound system property.
@DisabledIf("/32/.test(systemProperty.get('os.arch'))")
void disabledOn32BitArchitectures() {
    assertFalse(System.getProperty("os.arch").contains("32"));
}

@Test
@EnabledIf("'CI' == systemEnvironment.get('ENV')")
void onlyOnCiServer() {
    assertTrue("CI".equals(System.getenv("ENV")));
}

@Test // Multi-line script, custom engine name and custom reason.
@EnabledIf(value = {
                "load('nashorn:mozilla_compat.js')",
                "importPackage(java.time)",
                "",
                "var today = LocalDate.now()",
                "var tomorrow = today.plusDays(1)",
                "tomorrow.isAfter(today)"
            },
            engine = "nashorn",
            reason = "Self-fulfilling: {result}")
void theDayAfterTomorrow() {
    LocalDate today = LocalDate.now();
    LocalDate tomorrow = today.plusDays(1);
    assertTrue(tomorrow.isAfter(today));
}
脚本绑定

以下名称绑定到每个脚本上下文,因此在脚本中使用。访问器 通过简单的String get(String name)方法提供对类似Map结构的访问。

名称类型描述
systemEnvironmentaccessor操作系统环境变量访问器。
systemPropertyaccessorJVM 系统属性访问器。
JunitConfigurationParameteraccessor配置参数访问器。
JunitDisplayNameString测试或容器的显示名称。
junitTagsSet<String>所有分配给测试或容器的标记。
junitUniqueIdString测试或容器的唯一ID。

3.8. 标记和过滤

测试类和测试方法可以被@Tag注解标记。那些标记可以在后面被用来过滤 测试发现和执行

3.8.1. 标记的语法规则

  • 标记不能为null
  • trimmed 的标记不能包含空格。
  • trimmed 的标记不能包含IOS字符。
  • trimmed 的标记不能包含一下保留字符。
    • ,:逗号
    • (:左括号
    • ):右括号
    • &:& 符号
    • |:竖线
    • !:感叹号

📒 上述的”trimmed”指的是两端的空格字符被去除掉。

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

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

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

}

3.9. 测试实例生命周期

为了隔离地执行单个测试方法,以及避免由于不稳定的测试实例状态引发非预期的副作用,JUnit会在执行每个测试方法执行之前创建一个新的实例(参考 测试类和测试方法)。这个”per-method”测试实例生命周期是JUnit Jupiter的默认行为,这点类似于JUnit以前的所有版本。

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

如果你希望JUnit Jupiter在同一个实例上执行所有的测试方法,在你的测试类上加上注解@TestInstance(Lifecycle.PER_CLASS)即可。启用了该模式后,每一个测试类只会创建一次实例。因此,如果你的测试方法依赖实例变量存储的状态,你可能需要在@BeforeEach@AfterEach方法中重置状态。

"per-class"模式相比于默认的"per-method"模式有一些额外的好处。具体来说,使用了"per-class"模式之后,你就可以在非静态方法和接口的default方法上声明@BeforeAll@AfterAll。因此,"per-class"模式使得在@Nested测试类中使用@BeforeAll@AfterAll注解成为了可能。

如果你使用Kotlin编程语言来编写测试,你会发现通过将测试实例的生命周期模式切换到"per-class"更容易实现@BeforeAll@AfterAll方法。

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

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

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

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

但是请注意,通过JUnit Platform配置文件来设置默认的测试实例生命周期模式是一个更强大的解决方案,因为配置文件可以与项目一起被提交到版本控制系统中,因此可用于IDE和构建软件。

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

junit.jupiter.testinstance.lifecycle.default = per_class

⚠️ 如果没有做到应用一致的配置,更改默认 的测试实例生命周期模式可能会导致不可预测的结果和脆弱的构建。例如,如果构建将"per-class"语义配置为默认值,但是IDE中的测试却使用"per-method"的语义来执行,这样会增加在构建服务器上调试错误的难度。因此,建议更改JUnit Platform配置文件中的默认值,而不是通过JVM系统属性。

3.10. 嵌套测试

嵌套测试让测试编写者能够表示出几组测试用例之间的关系。下面来看一个精心设计的例子。

一个用于测试栈的嵌套测试套件

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

📒 @Nested测试类必须是非静态嵌套类(即内部类),并且可以有任意多层的嵌套。这些内部类被认为是测试类家族的正式成员,但有一个例外:@BeforeAll@AfterAll方法默认 不会工作。原因是Java不允许内部类中存在static成员。不过这种限制可以使用@TestInstance(Lifecycle.PER_CLASS)标注@Nested测试类来绕开(请参阅 测试实例生命周期)。

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

在之前的所有JUnit版本中,测试构造函数和方法是不允许传入参数的(至少不能使用标准的Runner实现)。JUnit Jupiter一个主要的改变是:允许给测试类的构造函数和方法传入参数。这带来了更大的灵活性,并且可以在构造函数和方法上使用依赖注入

ParameterResolver 为测试扩展定义了API,它可以在运行时动态 解析参数。如果一个测试的构造函数或者@Test@TestFactory@BeforeEach@AfterEach@BeforeAll或者 @AfterAll方法接收一个参数,这个参数就必须在运行时被一个已注册的ParameterResolver解析。

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

  • TestInfoParameterResolver:如果一个方法参数的类型是 TestInfoTestInfoParameterResolver将根据当前的测试提供一个TestInfo的实例用于填充参数的值。然后,TestInfo就可以被用来检索关于当前测试的信息,例如:显示名称、测试类、测试方法或相关的Tag。显示名称要么是一个类似于测试类或测试方法的技术名称,要么是一个通过@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() {
    }

}
  • RepetitionInfoParameterResolver:如果一个位于@RepeatedTest@BeforeEach或者@AfterEach方法的参数的类型是 RepetitionInfoRepetitionInfoParameterResolver会提供一个RepetitionInfo实例。然后,RepetitionInfo就可以被用来检索对应@RepeatedTest方法的当前重复以及总重复次数等相关信息。但是请注意,RepetitionInfoParameterResolver不是在@RepeatedTest的上下文之外被注册的。请参阅 重复测试示例
  • TestInfoParameterResolver:如果一个方法参数的类型是 TestReporterTestReporterParameterResolver会提供一个TestReporter实例。然后,TestReporter就可以被用来发布有关当前测试运行的其他数据。这些数据可以通过 TestExecutionListenerreportingEntryPublished()方法来消费,因此可以被IDE查看或包含在报告中。

在JUnit Jupiter中,你应该使用TestReporter来代替你在JUnit 4中打印信息到stdoutstderr的习惯。使用@RunWith(JUnitPlatform.class)会将报告的所有条目都输出到stdout中。

import java.util.HashMap;

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

class TestReporterDemo {

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

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

        testReporter.publishEntry(values);
    }

}

📒 其他的参数解析器必须通过@ExtendWith注册合适的 扩展 来明确地开启。

可以查看 RandomParametersExtension 获取自定义 ParameterResolver 的示例。虽然并不打算大量使用它,但它演示了扩展模型和参数解决过程中的简单性和表现力。MyRandomParametersTest演示了如何将随机值注入到@Test方法中。

@ExtendWith(RandomParametersExtension.class)
class MyRandomParametersTest {

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

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

}

对于真实的使用场景,请查看 MockitoExtensionSpringExtension 的源码。

3.12. 测试接口和默认方法

JUnit Jupiter允许将@Test@RepeatedTest@ParameterizedTest@TestFactoryTestTemplate@BeforeEach@AfterEach注解声明在接口的default方法上。如果 测试接口或测试类使用了@TestInstance(Lifecycle.PER_CLASS)注解(请参阅 测试实例生命周期),则可以在测试接口中的static方法或接口的default方法上声明@BeforeAll@AfterAll。下面来看一些例子。

@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

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

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

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

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

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

}
interface TestInterfaceDynamicTestsDemo {

    @TestFactory
    default Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test in test interface", () -> assertTrue(true)),
            dynamicTest("2nd dynamic test in test interface", () -> assertEquals(4, 2 * 2))
        );
    }

}

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

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

在测试类中,你可以通过实现这些测试接口来获取那些配置信息。

class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutionLogger, TestInterfaceDynamicTestsDemo {

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

}

运行TestInterfaceDemo,你会看到类似于如下的输出:

:junitPlatformTest
INFO  example.TestLifecycleLogger - Before all tests
INFO  example.TestLifecycleLogger - About to execute [dynamicTestsFromCollection()]
INFO  example.TimingExtension - Method [dynamicTestsFromCollection] took 13 ms.
INFO  example.TestLifecycleLogger - Finished executing [dynamicTestsFromCollection()]
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

Test run finished after 190 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         3 tests found           ]
[         0 tests skipped         ]
[         3 tests started         ]
[         0 tests aborted         ]
[         3 tests successful      ]
[         0 tests failed          ]

BUILD SUCCESSFUL

此功能的另一个可能的应用场景是为接口契约编写测试。例如,你可以编写测试,以了解Object.equalsComparable.compareTo的工作原理。

public interface Testable<T> {

    T createValue();

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

    T createNotEqualValue();

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

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

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

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

    T createSmallerValue();

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

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

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

}

在测试类中,你可以实现两个契约接口,从而继承相应的测试。当然,你还得实现那些抽象方法。

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

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

    @Override
    public String createSmallerValue() {
        return "bar"; // 'b' < 'f' in "foo"
    }

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

}

📒 上述测试仅仅作为例子,因此它们是不完整的。

3.13. 重复测试

在JUnit Jupiter中,我们可以使用@RepeatedTest注解并指定所需的重复次数来重复运行一个测试方法。每个重复测试的调用就像执行常规的@Test方法一样,完全支持相同的生命周期回调和扩展。

下面示例演示了如何声明一个会自动重复执行10次的测试方法repeatedTest()

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

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

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

  • {currentRepetition}: 当前的重复次数。

  • {totalRepetitions}: 总的重复次数。

一个特定重复的默认显示名称基于以下模式生成:"repetition {currentRepetition} of {totalRepetitions}"。因此,之前的repeatTest()例子的单个重复的显示名称将是:repetition 1 of 10, repetition 2 of 10,等等。如果你希望每个重复的名称中包含@RepeatedTest方法的显示名称,你可以自定义自己的模式或使用预定义的RepeatedTest.LONG_DISPLAY_NAME。后者等同于"{displayName} :: repetition {currentRepetition} of {totalRepetitions}",在这种模式下,repeatedTest()方法单次重复的显示名称长成这样:repeatedTest() :: repetition 1 of 10, repeatedTest() :: repetition 2 of 10,等等。

为了以编程方式获取有关当前重复和总重复次数的信息,开发人员可以选择将一个RepetitionInfo的实例注入到@RepeatedTest@BeforeEach@AfterEach方法中。

3.13.1. 重复测试示例

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

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

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

repeatedTestInGerman()演示了将重复测试的显示名称翻译成外语的能力 – 比如例子中的德语,所以结果看起来像:Wiederholung 1 von 5, Wiederholung 2 von 5,等等。

由于beforeEach()方法使用了@BeforeEach注解,所以在每次重复测试之前都会执行它。通过往方法中注入TestInfoRepetitionInfo,我们就有可能获得有关当前正在执行的重复测试的信息。启用INFO的日志级别,执行RepeatedTestsDemo可以看到如下的输出。

INFO: About to execute repetition 1 of 10 for repeatedTest
INFO: About to execute repetition 2 of 10 for repeatedTest
INFO: About to execute repetition 3 of 10 for repeatedTest
INFO: About to execute repetition 4 of 10 for repeatedTest
INFO: About to execute repetition 5 of 10 for repeatedTest
INFO: About to execute repetition 6 of 10 for repeatedTest
INFO: About to execute repetition 7 of 10 for repeatedTest
INFO: About to execute repetition 8 of 10 for repeatedTest
INFO: About to execute repetition 9 of 10 for repeatedTest
INFO: About to execute repetition 10 of 10 for repeatedTest
INFO: About to execute repetition 1 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 2 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 3 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 4 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 5 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 1 of 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 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 = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
    @DisplayName("Repeat!")
    void customDisplayName(TestInfo testInfo) {
        assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
    }

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

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

}

在启用了unicode主题的情况下使用ConsoleLauncherjunitPlatformTest Gradle插件时,执行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 ✔
│  ├─ 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 ✔

3.14. 参数化测试

参数化测试可以用不同的参数多次运行试。除了使用@ParameterizedTest 注解,它们的声明跟@Test的方法没有区别。此外,你必须声明至少一个参数源来给每次调用提供参数,然后在测试方法中消费 这些参数。

以下示例演示了使用@ValueSource注解指定String数组作为参数源的参数化测试。

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

执行上述方法时,每次调用会被分别报告。例如,ConsoleLauncher会打印类似下面的信息。

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

⚠️ 参数化测试目前是一个试验性功能。详细信息请参阅 试验性API 中的表格。

3.14.1. 必需的设置

为了使用参数化测试,你必须添加junit-jupiter-params依赖。详细信息请参考 依赖元数据

3.14.2 消费参数

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

  • 首先必须声明零个或多个索引参数
  • 接下来必须声明零个或多个聚合器
  • ParameterResolver提供的零个或多个参数必须声明为最后一个。

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

3.14.3. 参数源

Junit Jupiter提供一些开箱即用的 注解。接下来每个子章节将提供一个简要的概述和一个示例。更多信息请参阅 org.junit.jupiter.params.provider 包中的JavaDoc。

@ValueSource

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

@ValueSource支持以下类型的字面值:

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • java.lang.String
  • java.lang.Class

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

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertNotNull(argument);
}
@EnumSource

@EnumSource能够很方便地提供Enum常量。该注解提供了一个可选的names参数,你可以用它来指定使用哪些常量。如果省略了,就意味着所有的常量将被使用,就像下面的例子所示。

@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSource(TimeUnit timeUnit) {
    assertNotNull(timeUnit);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
    assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}

@EnumSource注解还提供了一个可选的mode参数,它能够细粒度地控制哪些常量将会被传递到测试方法中。例如,你可以从枚举常量池中排除一些名称或者指定正则表达式,如下面代码所示。

@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
void testWithEnumSourceExclude(TimeUnit timeUnit) {
    assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
    assertTrue(timeUnit.name().length() > 5);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
void testWithEnumSourceRegex(TimeUnit timeUnit) {
    String name = timeUnit.name();
    assertTrue(name.startsWith("M") || name.startsWith("N"));
    assertTrue(name.endsWith("SECONDS"));
}
@MethodSource

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

除非使用@TestInstance(Lifecycle.PER_CLASS)注解标注测试类,否则测试类中的工厂方法必须是static的。 而外部类中的工厂方法必须始终是static的。 此外,此类工厂方法不能接受任何参数。

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

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

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

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

如果你未通过@MethodSource明确提供工厂方法名称,则JUnit Jupiter将按照约定去搜索与当前@ParameterizedTest方法名称相同的工厂方法。下面来看一个例子:

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

static Stream<String> testWithSimpleMethodSourceHavingNoValue() {
    return Stream.of("foo", "bar");
}

同样支持基本类型的Stream(DoubleStreamIntStreamLongStream),如下面示例所示。

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

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

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

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

static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
        Arguments.of("foo", 1, Arrays.asList("a", "b")),
        Arguments.of("bar", 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#blankStrings")
    void testWithExternalMethodSource(String blankString) {
        // test with blank string
    }
}

class StringsProviders {

    static Stream<String> blankStrings() {
        return Stream.of("", " ", " \n ");
    }
}

@CsvSource

@CsvSource允许你将参数列表定义为以逗号分隔的值(即String类型的值)。

@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCsvSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

@CsvSource使用单引号'作为引用字符。请参考上述示例和下表中的'baz,qux'值。一个空的引用值''表示一个空的String;而一个完全的值被当成一个null引用。如果null引用的目标类型是基本类型,则会抛出一个ArgumentConversionException

示例输入生成的参数列表
@CsvSource({ "foo, bar" })"foo", "bar"
@CsvSource({ "foo, 'baz, qux'" })"foo", "baz, qux"
@CsvSource({ "foo, ''" })"foo", ""
@CsvSource({ "foo, " })"foo", null
@CsvFileSource

@CsvFileSource允许你使用类路径中的CSV文件。CSV文件中的每一行都会触发参数化测试的一次调用。

@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv")
void testWithCsvFileSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

two-column.csv

foo, 1
bar, 2
"baz, qux", 3

📒 与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为引号字符,请参考上面例子中的"baz,qux"值,一个空的带引号的值""表示一个空String,一个完全为的值被当成null引用,如果null引用的目标类型是基本类型,则会抛出一个ArgumentConversionException

@ArgumentsSource

@ArgumentsSource 可以用来指定一个自定义且能够复用的ArgumentsProvider

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

static class MyArgumentsProvider implements ArgumentsProvider {

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

3.14.4. 参数转换

扩展转换

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

隐式转换

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

例如,如果一个@ParameterizedTest方法声明了TimeUnit类型的参数,而实际上提供了一个String,此时字符串会被自动转换成对应的TimeUnit枚举常量。

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

String实例目前会被隐式地转换成以下目标类型:

目标类型类型示例
boolean/Boolean"true" → true
byte/Byte"1" → (byte) 1
char/Character"o" → 'o'
short/Short"1" → (short) 1
int/Integer"1" → 1
long/Long"1" → 1L
float/Float"1.0" → 1.0f
double/Double"1.0" → 1.0d
Enum subclass"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/" → new URL("https://junit.org/")
java.nio.charset.Charset"UTF-8"Charset.forName("UTF-8")
java.nio.file.Path"/path/to/file"Paths.get("/path/to/file")
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.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.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.util.Currency"JPY"Currency.getInstance("JPY")
java.util.Locale"en"new Locale("en")
java.util.UUID"d043e930-7b3b-48e3-bdbe-5a3ccfb833db"UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")
回退String-to-Object转换

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

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

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

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

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

public class Book {

    private final String title;

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

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

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

除了使用隐式转换参数,你还可以使用@ConvertWith注解来显式指定一个ArgumentConverter用于某个参数,例如下面代码所示。

@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithExplicitArgumentConversion(@ConvertWith(ToStringArgumentConverter.class) String argument) {
    assertNotNull(TimeUnit.valueOf(argument));
}

static class ToStringArgumentConverter extends SimpleArgumentConverter {

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

显式参数转换器意味着开发人员要自己去实现它。正因为这样,junit-jupiter-params仅仅提供了一个可以作为参考实现的显式参数转换器:JavaTimeArgumentConverter。你可以通过组合注解JavaTimeArgumentConverter来使用它。

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

3.14.5. 参数聚合

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

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

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

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

ArgumentsAccessor的一个实例被自动注入到 ArgumentsAccessor 类型的任何参数中

自定义聚合器

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

要使用自定义聚合器,只需实现ArgumentsAggregator接口并通过@AggregateWith注释将其注册到@ParameterizedTest方法的兼容参数中。当调用参数化测试时,聚合结果将作为相应参数的参数提供。

@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 {
}

3.14.6. 自定义显示名称

默认情况下,参数化测试调用的显示名称包含了该特定调用的索引和所有参数的String表示形式。不过,你可以通过@ParameterizedTest注解的name属性来自定义调用的显示名称,如下面代码所示。

@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> first=''{0}'', second={1}")
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCustomDisplayNames(String first, int second) {
}

使用ConsoleLauncher执行上面方法,你会看到类似于下面的输出。

Display name of container ✔
├─ 1 ==> first='foo', second=1 ✔
├─ 2 ==> first='bar', second=2 ✔
└─ 3 ==> first='baz, qux', second=3 ✔

自定义显示名称支持下面表格中的占位符。

占位符描述
{index}当前调用的索引 (1-based)
{arguments}完整的参数列表,以逗号分隔
{0}, {1}, …​单个参数

3.14.7. 生命周期和互操作性

参数化测试的每次调用拥有跟普通@Test方法相同的生命周期。例如,@BeforeEach方法将在每次调用之前执行。类似于 动态测试,调用将逐个出现在IDE的测试树中。你可能会在一个测试类中混合常规@Test方法和@ParameterizedTest方法。

你可以在@ParameterizedTest方法上使用ParameterResolver扩展。但是,被参数源解析的方法参数必须出现在参数列表的首位。由于测试类可能包含常规测试和具有不同参数列表的参数化测试,因此,参数源的值不会被生命周期方法(例如@BeforeEach)和测试类构造函数解析。

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

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

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

3.15. 测试模板

@TestTemplate 方法不是一个常规的测试用例,它是测试用例的模板。因此,它的设计初衷是用来被多次调用,而调用次数取决于注册提供者返回的调用上下文数量。所以,它必须结合 TestTemplateInvocationContextProvider 扩展一起使用。测试模板方法每一次调用跟执行常规@Test方法一样,它也完全支持相同的生命周期回调和扩展。关于它的用例请参阅 为测试模板提供调用上下文

3.16. 动态测试

JUnit Juppiter的 注解 章节描述的标准@Test注解跟JUnit 4中的@Test注解非常类似。两者都描述了实现测试用例的方法。这些测试用例都是静态的,因为它们是在编译时完全指定的,而且它们的行为不能在运行时被改变。假设提供了一种基本的动态行为形式,但其表达性却被故意地加以限制

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

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

任何由@TestFactory方法返回的Stream在调用stream.close()的时候会被正确地关闭,这样我们就可以安全地使用一个资源,例如:Files.lines()

@Test方法一样,@TestFactory方法不能是privatestatic的。但它可以声明被ParameterResolvers解析的参数。

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

⚠️ 动态测试生命周期

动态测试执行生命周期跟标准的@Test测试截然不同。具体而言,动态测试不存在任何生命周期回调。这意味着@BeforeEach@AfterEach方法以及它们相应的扩展回调函数对@TestFactory方法执行,而不是对每个动态测试执行。换言之,如果你从一个lambda表达式的测试实例中访问动态测试的字段,那么由同一个@TestFactory方法生成的各个动态测试执行之间的回调方法或扩展不会重置那些字段。

译者注:同一个@TestFactory所生成的n个动态测试,@BeforeEach@AfterEach只会在这n个动态测试开始前和结束后各执行一次,不会为每一个单独的动态测试都执行。

在JUnit Jupiter 中,动态测试必须始终由工厂方法创建;不过,在后续的发行版中,这可能会得到注册工具的补充。

⚠️ 动态测试目前是一个试验性功能。详细信息请参阅 试验性API 中的表格。

3.16.1. 动态测试示例

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

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

接下来五个方法是非常简单的例子,它们演示了生成一个DynamicTest实例的CollectionIterableIteratorStream。这些例子中大多数并不真正表现出动态行为,而只是为了证明原则上所支持的返回类型。然而,dynamicTestsFromStream()dynamicTestsFromIntStream()演示了为给定的一组字符串或一组输入数字生成动态测试是多么的容易。

下一个方法是真正意义上动态的。generateRandomNumberOfTests()实现了一个生成随机数的Iterator,一个显示名称生成器和一个测试执行器,然后将这三者提供给DynamicTest.stream()。因为generateRandomNumberOfTests()的非确定性行为会与测试的可重复性发生冲突,因此应该谨慎使用,这里只是用它来演示动态测试的表现力和强大。

最后一个方法使用DynamicContainer来生成动态测试的嵌套层次结构。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

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

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

class DynamicTestsDemo {

    // 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(true)),
            dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

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

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

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("A", "B", "C")
            .map(str -> dynamicTest("test" + str, () -> { /* ... */ }));
    }

    @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> generateRandomNumberOfTests() {

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

            Random random = new Random();
            int current;

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

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

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

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

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

    @TestFactory
    Stream<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()))
                ))
            )));
    }

}

3.17. 并行执行

默认情况下,JUnit Jupiter测试在单个线程中按顺序运行。要并行运行测试,例如 加速执行,自5.3版本开始作为可选择的功能被加入进来。要启用并行执行,只需将junit.jupiter.execution.parallel.enabled配置参数设置为true,例如 在junit-platform.properties中(请参阅其他选项的 配置参数)。

启用后,JUnit Jupiter引擎将根据提供的 配置 完全并行地在所有级别的测试树上执行测试,同时观察声明性 同步机制。 请注意,捕获标准输出/错误 功能需要单独开启。

⚠️ 并行测试执行目前是一项实验性功能。 你被邀请尝试并向JUnit团队提供反馈,以便他们可以 改进 并最终推广此功能。

3.17.1 配置

可以使用 ParallelExecutionConfigurationStrategy 配置所需并行度和最大池大小等属性。 JUnit平台提供了两种开箱即用的实现:dynamicfixed。 当然,你也可以实现一个custom的策略。

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

dynamic

根据可用处理器/核心数乘以junit.jupiter.execution.parallel.config.dynamic.factor配置参数(默认为1)计算所需的并行度。

fixed 强制使用junit.jupiter.execution.parallel.config.fixed.parallelism配置参数作为所需的并行度。

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

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

3.17.2 同步

org.junit.jupiter.api.parallel包中,JUnit Jupiter提供了两种基于注解的声明性机制,用于在不同测试中使用共享资源时更改执行模式并允许同步。

如果启用了并行执行,默认情况下会同时执行所有类和方法。你可以使用 @Execution 注解更改带注解的元素及其子元素(如果有)的执行模式。有以下两种模式:

SAME_THREAD

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

CONCURRENT

除非存在资源约束要强制在同一线程中执行,否则执行并发。

此外,@ResourceLock 注解允许声明测试类或测试方法使用需要同步访问的特定共享资源,以确保可靠的测试执行。

如果你并行运行下面示例中的测试,你会发现它们很不稳定,即有时通过而其他时间失败。因为它们所读取的资源在写入是存在竞争。

@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 canSetCustomPropertyToFoo() {
        System.setProperty("my.prop", "foo");
        assertEquals("foo", System.getProperty("my.prop"));
    }

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

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

除了用于唯一标记已使用资源的字符串之外,你还可以指定访问模式。需要对资源进行READ访问的两个测试可以彼此并行运行,除非有其他READ_WRITE访问模式的测试正在运行。


4. 运行测试

4.1. IDE支持

4.1.1. IntelliJ IDEA

IntelliJ IDEA 从 2016.2 版本开始支持在JUnit Platform上运行测试。详情请参阅 IntelliJ IDEA的相关博客。但是请注意,我们建议使用IDEA 2017.3或更新的版本,因为这些较新版本的IDEA会根据项目中使用的API版本自动下载这些JAR文件:junit-platform-launcherjunit-jupiter-enginejunit-vintage-engine

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

要想使用JUnit 5的不同版本(比如,5.3.0),你需要在类路径中引入相应版本的junit-platform-launcherjunit-jupiter-enginejunit-vintage-engine JAR文件。

添加Gradle依赖
// Only needed to run tests in a version of IntelliJ IDEA that bundles older versions
testRuntime("org.junit.platform:junit-platform-launcher:1.2.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.3.0")
testRuntime("org.junit.vintage:junit-vintage-engine:5.3.0")
添加Maven依赖
<!-- 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>
    <version>1.2.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.3.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <version>5.3.0</version>
    <scope>test</scope>
</dependency>

4.1.2. Eclipse

自从Eclipse Oxygen.1a(4.7.1a)版本发布开始,Eclipse IDE提供了对JUnit平台的支持。

有关在Eclipse中使用JUnit 5的更多信息,请参阅官方 Eclipse Project Oxygen.1a (4.7.1a) - New and Noteworthy 文档中的Eclipse support for JUnit 5 章节。

4.1.3. 其他 IDE

在本文写作之时,并没有其他任何IDE可以像IntelliJ IDEA和Eclipse一样可以直接在JUnit Platform上运行Java测试。但是,Junit团队提供了另外两种折中的方法让JUnit 5可以在其他的IDE上使用。你可以尝试手动使用 控制台启动器 或者通过 基于JUnit 4的Runner 来执行测试。

4.2. 构建工具支持

4.2.1. Gradle

4.6 开始,Gradle对在JUnit Platform执行测试提供了 本地化支持 。要启用此功能,你只需要build.gradle文件中的test任务声明里指定useJUnitPlatform()即可:

test {
    useJUnitPlatform()
}

同样支持通过tag和引擎过滤:

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

有关详细的配置选项,请参考 官方Gradle文档

⚠️ JUnit Platform Gradle 插件被弃用了

有JUnit team开发的基础junit-platform-gradle-plugin在 JUnit Platform 1.2 中被弃用,并且在1.3中不再支持。请使用标准的Gradle test任务代替。

配置参数

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

test {
    // ...
    systemProperty 'junit.jupiter.conditions.deactivate', '*'
    systemProperties = [
        'junit.jupiter.extensions.autodetection.enabled': 'true',
        'junit.jupiter.testinstance.lifecycle.default': 'per_class'
    ]
    // ...
}
配置测试引擎

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

要支持基于JUnit Jupiter的测试,配置一个JUnit Jupiter API的testCompile依赖以及JUnit Jupiter TestEnginetestComiple依赖,类似如下配置:

dependencies {
    testCompile("org.junit.jupiter:junit-jupiter-api:5.2.0")
    testRuntime("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}

只要你配置了一个JUnit 4的testCompile依赖和一个JUnit Vintage TestEnginetestRuntime依赖,JUnit Platform就可以运行基于JUnit 4的测试,类似如下配置:

dependencies {
    testCompile("junit:junit:4.12")
    testRuntime("org.junit.vintage:junit-vintage-engine:5.2.0")
}
配置日志记录(可选)

JUnit 使用了位于java.util.logging包(a.k.a.JUL)下的Java Logging API来发布警告和调式信息。关于配置选项,请参阅官方文档的 LogManager 章节。

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

test {
    systemProperty 'java.util.logging.manager', 'org.apache.logging.log4j.jul.LogManager'
}

其他的日志框架提供了不同的方式来重定向java.util.logging记录的信息。例如,对于 Logback,你可以通过向runtime类路径添加一个额外的依赖来启用JUL to SLF4J Bridge

4.2.2. Maven

📒 最初由JUnit团队开发的自定义junit-platform-surefire-provider已被弃用,并计划在JUnit Platform 1.4中删除。 请改用Maven Surefire的原生支持。

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

配置测试引擎

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

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

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.0</version>
        </plugin>
    </plugins>
</build>
...
<dependencies>
    ...
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.3.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine </artifactId>
        <version>5.3.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>
...

只要你在JUnit 4和JUnit Vintage TestEngine实现上配置test范围依赖项,Maven Surefire就可以运行基于JUnit 4的测试以及Jupiter测试,类似于以下配置:

...
<build>
    <plugins>
        ...
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.0</version>
        </plugin>
    </plugins>
</build>
...
<dependencies>
    ...
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
    	  <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.3.0</version>
        <scope>test</scope>
    </dependency>    
</dependencies>
...
按测试类名过滤

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

  • **/Test*.java

  • **/*Test.java

  • **/*Tests.java

  • **/*TestCase.java

此外,它默认会排除所有内嵌类(包括静态成员类)。

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

覆盖Maven Surefire的排除规则

...
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.0</version>
            <configuration>
                <excludes>
                    <exclude/>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>
...

有关详细信息,请参阅 Inclusions and Exclusions of Tests 的文档。

按Tag过滤

使用以下配置属性,你可以通过Tag来过滤测试。

  • 要包含一个 tags 或者 tag expressions,可以使用groups
  • 要排除一个 tags 或者 tag expressions,可以使用excludedGroups
...
<build>
    <plugins>
        ...
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.0</version>
            <configuration>
                <groups>acceptance | !feature-a</groups>
                <excludedGroups>integration, regression</excludedGroups>
            </configuration>
        </plugin>
    </plugins>
</build>
...

配置参数

你可以使用configurationParameters属性并以Java Properties文件的语法提供键值对来设置配置参数,从而影响测试发现和执行。

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

4.2.3. Ant

Ant1.10.3开始,引入了一个新的 junitlauncher 任务来提供在JUnit Platform上加载测试的本地化支持。junitlauncher任务单独负责加载JUnit Platform并将选定的测试集合传递给它。然后,JUnit Platform委托已注册的测试引擎来发现和执行测试。

junitlauncher任务尝试尽可能地与本地Ant构造(如 资源集合)保持一致,以便用户选择他们想要由测试引擎执行的测试。与许多其他核心Ant任务相比,这赋予了该任务一致且自然的感觉。

📒 Ant 1.10.3中提供的junitlauncher任务版本为启动JUnit平台提供了基本的最小支持。其他增强功能(包括支持在单独的JVM中分支测试)将在随后的Ant版本中提供。

junit5-jupiter-starter-ant 项目中的build.xml文件演示了如何使用它,你可以以它作为一个起点。

基本用法

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

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

<!-- ... -->

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

test 元素允许你指定你想要选择和执行的单个测试类。classpath元素允许你指定用于启动JUnit平台的类路径。这个类路径也将用于定位属于执行部分的测试类。

以下示例演示如何配置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.3. 控制台启动器

ConsoleLauncher 是一个Java的命令行应用程序,它允许你通过命令行来启动JUnit Platform。例如,它可以用来运行JUnit Vintage和JUnit Jupiter测试,并在控制台中打印测试结果。

junit-platform-console-standalone-1.2.0.jar这个包含了所有依赖的可执行的jar包已经被发布在Maven仓库中,它位于 junit-platform-console-standalone目录下,你可以 运行 独立的ConsoleLauncher,如下所示。

java -jar junit-platform-console-standalone-1.2.0.jar<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 将以状态码1退出。 如果未发现任何测试并且提供了--fail-if-no-tests命令行选项,则ConsoleLauncher将以状态代码2退出。否则退出代码为0。

4.3.1. Options

Usage: ConsoleLauncher [-h] [--disable-ansi-colors] [--fail-if-no-tests] [--scan-modules]
                       [--scan-classpath[=PATH[;|:PATH...]]]... [--details=MODE]
                       [--details-theme=THEME] [--reports-dir=DIR]
                       [--config=KEY=VALUE]... [--exclude-package=PKG]...
                       [--include-package=PKG]... [-c=CLASS]... [-cp=PATH[;|:PATH...]]...
                       [-d=DIR]... [-e=ID]... [-E=ID]... [-f=FILE]... [-m=NAME]...
                       [-n=PATTERN]... [-N=PATTERN]... [-o=NAME]... [-p=PKG]...
                       [-r=RESOURCE]... [-t=TAG]... [-T=TAG]... [-u=URI]...
Launches the JUnit Platform from the console.
  -h, --help                 Display help information.
      --disable-ansi-colors  Disable ANSI colors in output (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. 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: unicode.
      -cp, --classpath, --class-path=PATH[;|:PATH...]
                             Provide additional classpath entries -- for example, for adding
                               engines and their dependencies. This option can be repeated.
      --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).
      --scan-modules         EXPERIMENTAL: Scan all resolved modules for test discovery.
  -o, --select-module=NAME   EXPERIMENTAL: Select single module for test discovery. This
                               option can be repeated.
      --scan-classpath, --scan-class-path[=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.
  -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.
  -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.
  -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.
      --config=KEY=VALUE     Set a configuration parameter for test discovery and execution.
                               This option can be repeated.

4.3.2. 参数文件 (@-files)

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

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

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

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

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

你可以通过使用额外的@符号转义以@开始的字符的参数。例如,@@somearg将成为@somearg,不会被扩展。

4.4. 使用JUnit 4运行JUnit Platform

JunitPlatform 运行器是一个基于JUnit 4的Runner,它让你能够在一个JUnit 4环境中的JUnit Platform上运行那些编程模型被支持的任何测试。例如一个JUnit Jupiter测试类。

如果某个类被标注了@RunWith(JUnitPlatform.class)注解,它就可以在那些支持JUnit 4但是还不支持JUnit Platform的IDE和构建系统中直接运行。

📒 由于JUnit Platform具备一些JUnit 4不具备的功能,因此运行器只能部分支持JUnit Platform的功能,特别是在报告方面(请参阅 显示名称与技术名称)。但是就目前来说,JUnitPlatform运行器是一个简单的入门方式。

4.4.1. 设置

你需要在类路径中添加以下的组件和它们的依赖。可以在 依赖元数据 中查看关于group ID, artifact ID 和版本的详细信息。

显式依赖
  • junit-4.12.jartest 作用域内:使用JUnit 4运行测试。
  • junit-platform-runnertest 作用域内:JUnitPlatform运行器的位置。
  • junit-jupiter-apitest 作用域内:编写测试的API,包括 @Test 等。
  • junit-jupiter-enginetest runtime 范围内:JUnit Jupiter引擎API的实现。
可传递的依赖
  • junit-platform-launchertest 作用域内
  • junit-platform-enginetest 作用域内
  • junit-platform-commonstest 作用域内
  • opentest4jtest 作用域内

4.4.2. 展示名称与技术名称

默认情况下,显示名称 会被使用在测试产出物上,但是当JUnitPlatform运行器使用Gradle或者Maven等构建工具来运行测试时,生成的测试报告通常需要包含测试产出物的技术名称(例如,使用完整类名),而不是像测试类的简单名称或包含特殊字符的自定义显示名称这种较短的显示名称。为了在测试报告中使用技术名称,在@RunWith(JUnitPlatform.class)注解旁声明 @UseTechnicalNames注解即可。

4.4.3. 单一测试类

使用JUnitPlatform运行器的方式之一是直接在测试类上添加 @RunWith(JUnitPlatform.class)注解。请注意,以下示例中的测试方法使用的注解是org.junit.jupiter.api.Test(JUnit Jupiter),而不是 org.junit.Test(JUnit Vintage)。同时,这个类中的测试用例必须为 public,否则,IDE不能将其识别为一个JUnit 4的测试类。

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

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

@RunWith(JUnitPlatform.class)
public class JUnit4ClassDemo {

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

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

}

4.4.4. 测试套件

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

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

@RunWith(JUnitPlatform.class)
@SelectPackages("example")
public class JUnit4SuiteDemo {
}

JUnit4SuiteDemo类会发现并运行所有位于example包及其子包下的测试。默认情况下,它只包含类名以Test开始或者以Test或`Tests结束的测试类。

📒 附加配置选项
除了@SelectPackages之外,还有很多配置选项可以用来发现和过滤测试。详细内容请参考 Javadoc.

4.5. 配置参数

除了告诉平台要包含哪些测试类、测试引擎以及要扫描哪些包等之外,有时还需要提供额外的自定义配置参数,该参数特定于特定的测试引擎。例如,JUnit Jupiter TestEngine支持以下用例中的配置参数

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

  1. LauncherDiscoveryRequestBuilder中的configurationParameter()configurationParameters()方法可以用来构建提供给 Launcher API 的请求。当使用JUnit Platform提供的某一种工具运行测试时,你可以采用如下所示的方式指定配置参数:
  2. JVM 系统属性。
  3. JUnit Platform配置文件:该文件命名为junit-platform.properties,位于类路径根目录下,并遵循Java Properties文件的语法。

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

4.6. 标记表达式

标记表达式是运算符|的布尔表达式。另外,可用于调整运算符优先级。

表 1. 运算符(按优先顺序降序排列)

运算符含义关联性
!right
&left
|left

如果你在多个维度上标记测试,tag expressions 可帮助您选择要执行的测试。通过测试类型(例如,micro, integration, end-to-end)和特征(例如,foobarbaz)标记以下表达式可能很有用。

标记表达式选择
foofoo的所有测试
bar | bazbarbaz的所有测试
bar & bazbarbaz的测试交集
foo & !end-to-endfoo的所有测试,但不是端到端测试
(micro | integration) & (foo | baz)foobaz的所有微测试或集成测试

4.7. 捕获标准输出/错误

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

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

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

⚠️ 捕获输出目前是一项实验性功能。 你被邀请尝试并向JUnit团队提供反馈,以便他们可以 改进 并最终推广此功能。


5. 扩展模型

5.1. 概述

不同于JUnit4中的Runner@Rule以及@ClassRule等多个扩展点,JUnit Jupiter的扩展模型由一个连贯的概念组成:ExtensionAPI。但是,需要注意的是 Extension本身也只是一个标记接口。

5.2. 注册扩展

JUnit Jupiter中的扩展可以通过 @ExtenWith 注解进行声明式注册,或者通过 @RegisterExtension 注解进行编程式注册,再或者通过Java的 ServiceLoader 机制自动注册。

5.2.1. 声明式扩展注册

开发者可以通过在测试接口、测试类、测试方法或者自定义的 组合注解 上标注@ExtendWith(...)并提供要注册扩展类的引用,从而以声明式 的方式注册一个或多个扩展。

例如,要给某个测试方法注册一个自定义的RandomParametersExtension,你可以参照如下的方式标注该方法。

@ExtendWith(RandomParametersExtension.class)
@Test
void test(@Random int i) {
    // ...
}

若要为某个类或者其子类注册一个自定义的MockitoExtension,将注解添加到测试类上即可。

@ExtendWith(RandomParametersExtension.class)
class MyTests {
    // ...
}

多个扩展类的注册可以通过如下形式完成。

@ExtendWith({ FooExtension.class, BarExtension.class })
class MyFirstTests {
    // ...
}

还有另外一种方式来注册多个扩展类,如下面代码所示。

@ExtendWith(FooExtension.class)
@ExtendWith(BarExtension.class)
class MySecondTests {
    // ...
}

📒 扩展注册顺序
通过@ExtendWith以声明方式注册的扩展将按照它们在源代码中声明的顺序执行。例如,MyFirstTestsMySecondTests中的测试执行将按照FooExtensionBarExtension的实际顺序进行扩展。

5.2.2. 编程式扩展注册

开发人员可以通过编程的 方式来注册扩展,只需要将测试类中的属性字段使用 @RegisterExtension 注解标注即可。

当一个扩展通过 @ExtenWith 声明式注册后,它就只能通过注解配置。相比之下,当通过@RegisterExtension注册扩展时,我们可以通过编程 的方式来配置扩展 – 例如,将参数传递给扩展的构造函数、静态工厂方法或构建器API。

📒 @RegisterExtension 字段不能为privatenull (在评估阶段) ,但可以是static或非静态。

静态字段

如果一个@RegisterExtension字段是static的,该扩展会在那些在测试类中通过@ExtendWith进行注册的扩展之后被注册。这种静态扩展 在扩展API的实现上没有任何限制。因此,通过静态字段注册的扩展可能会实现类级别和实例级别的扩展API,例如BeforeAllCallbackAfterAllCallbackTestInstancePostProcessor,同样还有方法级别的扩展API,例如BeforeEachCallback等等。

在下面的例子中,测试类中的server字段通过使用WebServerExtension所支持的构建器模式以编程的方式进行初始化。已经配置的WebServerExtension将在类级别自动注册为一个扩展 - 例如,要在测试类中所有测试方法运行之前启动服务器,以及在所有测试完成后停止服务器。此外,使用@BeforeAll@AfterAll标注的静态生命周期方法以及@BeforeEach@AfterEach@Test标注的方法可以在需要的时候通过server字段访问该扩展的实例。

一个通过静态字段注册的扩展:

class WebServerDemo {

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

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

Kotlin编程语言没有static字段的概念。但我们可以使用注解来让编译器生成静态字段。如前文说述,@RegisterExtension标注的字段不能为privatenull,因此我们不能在Kotlin中使用会生成private字段@JvmStatic注解,所以我们就只能使用@JvmField注解了。

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

class KotlinWebServerDemo {

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

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

如果@RegisterExtension字段是非静态的(例如,一个实例字段),那么该扩展将在测试类实例化之后被注册,并且在每个已注册的TestInstancePostProcessor被赋予后处理测试实例的机会之后(可能给被标注的字段注入要使用的扩展实例)。因此,如果这样的实例扩展 实现了诸如BeforeAllCallbackAfterAllCallbackTestInstancePostProcessor这些类级别或实例级别的扩展API,那么这些API将不会正常执行。默认情况下,实例扩展将在那些通过@ExtendWith在方法级别注册的扩展之后被注册。但是,如果测试类是使用了@TestInstance(Lifecycle.PER_CLASS)配置,实例扩展将在它们之前被注册。

在下面的例子中,通过调用自定义lookUpDocsDir()方法并将结果提供给DocumentationExtension中的静态forPath()工厂方法,从而以编程的方式初始化测试类中的docs字段。配置的DocumentationExtension将在方法级别自动被注册为扩展。另外,@BeforeEach@AfterEach@Test方法可以在需要的时候通过docs字段访问扩展的实例。

一个通过静态字段注册的扩展:

class DocumentationDemo {

    static Path lookUpDocsDir() {
        // return path to docs dir
    }

    @RegisterExtension
    DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());

    @Test
    void generateDocumentation() {
        // use this.docs ...
    }
}

5.2.3. 自动扩展注册

除了 声明式扩展注册编程式扩展注册 支持使用注解,JUnit Jupiter还支持通过Java的java.util.ServiceLoader机制进行全局扩展注册,采用这种机制后会自动的检测classpath下的第三方扩展,并自动完成注册。

具体来说,自定义扩展可以通过在org.junit.jupiter.api.extension.Extension文件中提供其全类名来完成注册,该文件位于其封闭的JAR文件中的/META-INF/services目录下。

启用自动扩展检测

自动检测是一种高级特性,默认情况下它是关闭的。要启用它,只需要在配置文件中将 junit.jupiter.extensions.autodetection.enabled配置参数 设置为 true即可。该参数可以作为JVM系统属性、或作为一个传递给LauncherLauncherDiscoveryRequest中的配置参数、再或者通过JUnit Platform配置文件(详情请参阅 配置参数)来提供。

例如,要启用扩展的自动检测,你可以在启动JVM时传入如下系统参数。

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

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

5.2.4. 扩展继承

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

5.3. 条件测试执行

ExecutionCondition 定为程序化的条件测试执行定义了ExtensionAPI。

每个容器(例如测试类)都会对ExecutionCondition进行解析,从而确定是否应该根据提供的ExtensionContext执行其包含的所有测试。类似地,ExecutionCondition会被每个测试解析,从而确定是否应该根据提供的ExtensionContext执行给定的测试方法。

当多个ExecutionCondition扩展被注册时,只要有一个条件被禁用,容器或测试就会被禁用。所以,不能保证每个条件都会被解析,因为其中某个扩展可能已经导致容器或测试被禁用了。也就是说,条件的解析机制类似于短路 或(符号为||)操作。

有关具体示例,请参阅 DisabledCondition@Disabled 的源码。

5.3.1. 禁用条件

有时候,在没有明确的条件被激活的情况下运行测试套件可能更有用。例如,你可能想要运行某些即便被标注了@Disable的测试,从而观察这些测试是否一直是失败的。此时只需为junit.jupiter.conditions.deactivate配置参数提供一个匹配模式,以指定当前测试运行应停用哪些条件(即不被解析)。该匹配模式可以作为JVM系统属性、或作为一个传递给LauncherLauncherDiscoveryRequest中的配置参数、再或者通过JUnit Platform配置文件(详情请参阅 配置参数)来提供。

例如,要停用JUnit的@Disable条件,你可以在JVM启动时传入系统参数完成:

-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition
模式匹配语法

如果junit.jupiter.conditions.deactivate模式仅由星号(*)组成,则所有条件都将被禁用。 否则,该模式将用于匹配每个注册的条件的完整的类名(FQCN)。 模式中的点(.)会匹配FQCN中的点(.)或美元符号($)。 星号(*)匹配FQCN中的一个或多个字符。模式中的所有其他字符将与FQCN一对一匹配。

例如:

  • *: 禁用所有条件。
  • org.junit.*: 禁用org.junit基础包及子包下的所有条件。
  • *.MyCondition: 禁用MyCondition类中的每个条件。
  • *System*: 禁用其简单类名包含System的类中的每个条件。
  • org.example.MyCondition: 禁用FQCN为org.example.MyCondition的条件。

5.4. 测试实例工厂

TestInstanceFactory 为希望创建测试类实例的Extensions定义了API。

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

如果没有注册任何TestInstanceFactory,框架将只调用测试类的唯一构造方法来初始化它,并可能通过已注册的ParameterResolver来解析构造方法参数。

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

⚠️ 在一个特定测试类中注册多个实现了TestInstanceFactory的扩展将会在该类、它的子类以及它的内嵌类的所有测试方法中引发异常。请注意,在超类或者封闭类中(即,在@Nested测试类的情况下)注册的任何TestInstanceFactory也都会被继承。所以,开发者有责任去确保在一个特定的测试类中只注册一个TestInstanceFactory

5.5. 测试实例后处理

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

常见的用法涵盖了诸如将依赖注入到测试实例中,在测试实例中调用自定义的初始化方法等。

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

5.6. 参数解析

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

如果测试构造器或者@Test@TestFactory@BeforeEach@AfterEach@BeforeAll或者@AfterAll方法接收参数,则必须在运行时通过ParameterResolver解析 该参数。开发人员可以使用内置的ParameterResolver(参考 TestInfoParameterResolver)或 自己注册。一般而言,参数可能被按照其名称类型注解 或任何一种上述方式的组合所解析。具体示例可以参照 CustomTypeParameterResolverCustomAnnotationParameterResolver 的源码。

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

因此,提供给ParameterResolver实现的 ParameterContext API包含以下用于正确查找参数注释的便捷方法。强烈建议扩展开发人员使用这些方法,而不去使用java.lang.reflect.Parameter中提供的方法,从而避免JDK中的这个错误。

  • boolean isAnnotated(Class<? extends Annotation> annotationType)
  • Optional<A> findAnnotation(Class<A> annotationType)
  • List<A> findRepeatableAnnotations(Class<A> annotationType)

5.7. 测试生命周期回调

下列接口定义了用于在测试执行生命周期的不同阶段来扩展测试的API。关于每个接口的详细信息,可以参考后续章节的示例,也可以查阅 org.junit.jupiter.api.extension 包中的Javadoc。

📒 实现多个扩展API
扩展开发人员可以选择在单个扩展中实现任意数量的上述接口。具体示例请参阅 SpringExtension 的源代码。

5.7.1. 测试执行之前和之后的回调

BeforeTestExecutionCallbackAfterTestExecutionCallback 分别为Extensions定义了添加行为的API,这些行为将在执行测试方法之前之后立即执行。因此,这些回调非常适合于定时器、跟踪器以及其他类似的场景。如果你需要实现围绕@BeforeEach@AfterEach方法调用的回调,实现BeforeEachCallbackAfterEachCallback即可。

以下示例展示了如何使用这些回调来统计和记录测试方法的执行时间。TimingExtension同时实现了BeforeTestExecutionCallbackAfterTestExecutionCallback接口,从而给测试执行进行计时和记录。

一个为测试方法执行计时和记录的扩展
import java.lang.reflect.Method;
import java.util.logging.Logger;

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final Logger LOG = Logger.getLogger(TimingExtension.class.getName());

    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {
        getStore(context).put(context.getRequiredTestMethod(), System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        Method testMethod = context.getRequiredTestMethod();
        long start = getStore(context).remove(testMethod, long.class);
        long duration = System.currentTimeMillis() - start;

        LOG.info(() -> String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
    }

    private Store getStore(ExtensionContext context) {
        return context.getStore(Namespace.create(getClass(), context));
    }

}

由于TimingExtensionTests类通过@ExtendWith注册了TimingExtension,所以,测试将在执行时应用这个计时器。

一个使用示例TimingExtension的测试类
@ExtendWith(TimingExtension.class)
class TimingExtensionTests {

    @Test
    void sleep20ms() throws Exception {
        Thread.sleep(20);
    }

    @Test
    void sleep50ms() throws Exception {
        Thread.sleep(50);
    }

}

以下是运行TimingExtensionTests时生成的日志记录示例。

INFO: Method [sleep20ms] took 24 ms.
INFO: Method [sleep50ms] took 53 ms.

5.8. 异常处理

TestExecutionExceptionHandlerExtensions定义了异常处理的API,从而可以处理在执行测试时抛出的异常。

下面的例子展示了一个扩展,它将吃掉所有的IOException,但会重新抛出任何其他类型的异常。

一个异常处理扩展
public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {

    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
            throws Throwable {

        if (throwable instanceof IOException) {
            return;
        }
        throw throwable;
    }
}

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

当至少有一个 TestTemplateInvocationContextProvider 被注册时,标注了 @TestTemplate 的方法才能被执行。每个这样的provider负责提供一个 TestTemplateInvocationContext 实例的Stream。每个上下文都可以指定一个自定义的显示名称和一个额外的扩展名列表,这些扩展名仅用于下一次调用 @TestTemplate 方法。

以下示例展示了如何编写测试模板以及如何注册和实现一个 TestTemplateInvocationContextProvider

一个附带扩展名的测试模板
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String parameter) {
    assertEquals(3, parameter.length());
}

static class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {
    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return true;
    }

    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
        return Stream.of(invocationContext("foo"), invocationContext("bar"));
    }

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

在这个例子中,测试模板将被调用两次。调用的显示名称是调用上下文指定的”foo”和”bar”。每个调用都会注册一个自定义的 ParameterResolver 用于解析方法参数。下面是使用ConsoleLauncher时产生的输出信息。

└─ testTemplate(String) ✔
   ├─ foo ✔
   └─ bar ✔

TestTemplateInvocationContextProvider 扩展API主要用于实现不同类型的测试,这些测试依赖于某个类似于测试的方法的重复调用(尽管它们不在同一个上下文中)。 例如,使用不同的参数,以不同的方式准备测试类实例,或多次调用而不修改上下文。请参阅 重复测试参数化测试 的实现,它们都使用了该扩展点来提供其相关的功能。

5.10. 在扩展中保持状态

通常,扩展只实例化一次。随之而来的相关问题是:开发者如何能够在两次调用之间保持扩展的状态?ExtensionContext API提供了一个Store用来解决这一问题。扩展可以将值放入Store中供以后检索。请参阅 TimingExtension 了解如何使用具有方法级作用域的Store。要注意,在测试执行期间,被存储在一个ExtensionContext中的值在周围其他的ExtensionContext中是不可用的。由于ExtensionContexts可能是嵌套的,因此内部上下文的范围也可能受到限制。请参阅相应的Javadoc来了解有关通过 Store 存储和检索值的方法的详细信息。

5.11. 在扩展中支持的实用程序

junit-platform-commons公开了一个名为 的包,它包含了用于处理注解、类、反射和类路径扫描任务且正在维护中的实用工具方法。TestEngineExtension开发人员(authors)应该被鼓励去使用这些方法,以便与JUnit Platform的行为保持一致。

5.11.1. 注解支持

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

5.11.2. 类支持

ClassSupport提供静态工具方法来处理类(即java.lang.Class的实例)。有关详细信息,请参阅JavaDoc的 ClassSupport

5.11.3. 反射支持

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

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

当执行包含一个或多个测试方法的测试类时,除了用户提供的测试和生命周期方法外,还会调用大量的回调函数。 下图说明了用户提供的代码和扩展代码的相对顺序。

用户代码和扩展代码

用户提供的测试和生命周期方法以橙色表示,扩展提供的回调代码由蓝色显示。灰色框表示单个测试方法的执行,并将在测试类中对每个测试方法重复执行。

下表进一步解释了 用户代码和扩展代码 图中的十二个步骤。

步骤接口/注解描述
1接口org.junit.jupiter.api.extension.BeforeAllCallback执行所有容器测试之前执行的扩展代码
2注解org.junit.jupiter.api.BeforeAll执行所有容器测试之前执行的用户代码
3接口org.junit.jupiter.api.extension.BeforeEachCallback每个测试执行之前执行的扩展代码
4注解org.junit.jupiter.api.BeforeEach每个测试执行之前执行的用户代码
5接口org.junit.jupiter.api.extension.BeforeTestExecutionCallback测试执行之前立即执行的扩展代码
6注解org.junit.jupiter.api.Test真实测试方法的用户代码
7接口org.junit.jupiter.api.extension.TestExecutionExceptionHandler用于处理测试期间抛出的异常的扩展代码
8接口org.junit.jupiter.api.extension.AfterTestExecutionCallback测试执行后立即执行的扩展代码
9注解org.junit.jupiter.api.AfterEach每个执行测试之后执行的用户代码
10接口org.junit.jupiter.api.extension.AfterEachCallback每个执行测试之后执行的扩展代码
11注解org.junit.jupiter.api.AfterAll执行所有容器测试之后执行的用户代码
12接口org.junit.jupiter.api.extension.AfterAllCallback执行所有容器测试之后执行的扩展代码

在最简单的情况下,只有实际的测试方法被执行(步骤6); 所有其他步骤都是可选的,具体包含的步骤将取决于是否存在用户代码或对相应生命周期回调的扩展支持。有关各种生命周期回调的更多详细信息,请参阅每个注解和扩展各自的JavaDoc。


6. 从JUnit4迁移

虽然JUnit Jupiter编程模型和扩展模型本身不支持JUnit 4中的RulesRunners等特性,但我们不期望源码维护者为了迁移到JUnit Jupiter,而必须更新其现有的所有测试、测试扩展以及自定义构建测试基础设施。

然而,JUnit通过一个JUnit Vintage测试引擎 提供了一条平缓的迁移路径,该引擎允许使用JUnit Platform基础设施执行基于JUnit 3和JUnit 4的现有测试。由于所有JUnit Jupiter特有的类和注解都位于新的org.junit.jupiter基础包中,因此在类路径中同时使用JUnit 4和JUnit Jupiter不会导致任何冲突。所以,保持现有的JUnit 4测试和JUnit Jupiter测试是安全的。除此之外,JUnit团队会持续为JUnit 4.x 基线提供维护和错误修复的版本,所以开发人员有足够的时间按照自己的计划迁移到JUnit Jupiter。

6.1. 在 JUnit Platform 上运行JUnit4 测试

只要确保junit-vintage-engine包存在于你的测试运行时路径下,基于JUnit 3和 JUnit 4的测试将自动被JUnit Platform启动器拾取。

要想了解如何使用Gradle和Maven完成此操作,请参阅示例工程 junit5-samples

6.1.1. 类别支持

对于使用@Category注解的测试类或方法,JUnit Vintage 测试引擎将该类别的完全限定类名作为相应测试标识符的标记。例如,如果一个测试方法使用了@Category(Example.class)注解,它将被标记为"com.acme.Example"。与JUnit 4中的Categories runner类似,我们可以使用该信息在执行发现的测试之前对其进行过滤(详细信息请参阅 运行测试)。

6.2. 迁移技巧

以下是在将现有JUnit 4测试迁移到JUnit Jupiter时必须注意的事项。

  • org.junit.jupiter.api包中的注解。

  • org.junit.jupiter.api.Assertions类中的断言。

  • org.junit.jupiter.api.Assumptions类中的假设。

  • @Before@After已经不存在;取而代之的是@BeforeEach@AfterEach

  • @BeforeClass@AfterClass已经不存在;取而代之的是@BeforeAll@AfterAll

  • @Ignore 已经不存在:取而代之的是 @Disabled
  • @Category 已经不存在:取而代之的是 @Tag
  • @RunWith 已经不存在:取而代之的是@ExtendWith
  • @Rule@ClassRule已经不存在;取而代之的是@ExtendWith;关于部分规则的支持请参阅后续章节。

6.3. 对JUnit4规则的有限支持

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

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中一样,规则注解的字段跟方法一样是被支持的。通过在测试类使用这些类级别的扩展,可以保留 遗留代码库中的规则实现,其中包括JUnit4规则导入语句。

这种有限的Rule支持形式可以通过类级的注解org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport来开启。该注解是一个组合注解,它会启用所有支持迁移的扩展:VerifierSupportExternalResourceSupportExpectedExceptionSupport

然而,如果你打算开发一个新的JUnit 5扩展,请使用JUnit Jupiter的新扩展模型,而不要再去使用JUnit 4中基于Rule的模型。

⚠️ JUnit Jupiter中的JUnit 4 Rule支持目前是一个实验性功能。详细信息请参阅 试验性API


7. 高级主题

7.1 JUnit Platform启动器API

JUnit 5的主要目标之一是让JUnit与其编程客户端(构建工具和IDE)之间的接口更加强大和稳定。目的是将发现和执行测试的内部构件和外部必需的所有过滤和配置分离开来。

JUnit 5引入了Launcher的概念,它可以被用来发现、过滤和执行测试。此外,诸如 Spock、Cucumber和FitNesse等第三方测试库都可以通过提供自定义的 TestEngine 来集成到JUnit 5平台的启动基础设施中。

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

junit-platform-console 项目中的 ConsoleLauncher 就是一个具体的使用例示。

7.1.1 发现测试

测试发现 作为平台本身的一个专用功能而引入,会(希望能够)在很大程度上解决过去IDE和构建工具难以识别测试类和测试方法的问题。

使用示例:

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 org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
    .selectors(
        selectPackage("com.example.mytests"),
        selectClass(MyTestClass.class)
    )
    .filters(
        includeClassNamePatterns(".*Tests")
    )
    .build();

Launcher launcher = LauncherFactory.create();

TestPlan testPlan = launcher.discover(request);

目前,测试发现的搜索范围涵盖了类、方法、包中的所有类,甚至所有类路径中的测试。测试发现会贯穿所有参与的测试引擎。

生成的TestPlan是符合LauncherDiscoveryRequest对象的所有引擎、类、和测试方法的结构化(只读)描述。客户端可以遍历树,检索节点的详细信息,并获取到原始源的链接(如类,方法或文件位置)。测试计划中的每个节点都有一个唯一的ID,可以用它来调用特定的测试或一组测试。

7.1.2 执行测试

要执行测试,客户端可以使用与发现阶段相同的LauncherDiscoveryRequest,或者创建一个新的请求。测试进度和报告可以通过使用Launcher注册一个或多个 TestExecutionListener 实现来获取,如下面例子所示。

LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
    .selectors(
        selectPackage("com.example.mytests"),
        selectClass(MyTestClass.class)
    )
    .filters(
        includeClassNamePatterns(".*Tests")
    )
    .build();

Launcher launcher = LauncherFactory.create();

// 注册一个你选择的监听器
TestExecutionListener listener = new SummaryGeneratingListener();
launcher.registerTestExecutionListeners(listener);

launcher.execute(request);

execute()方法没有返回值,但你可以轻松地使用监听器将最终结果聚合到你自己的对象中。相关示例请参阅 SummaryGeneratingListener

7.1.3 插入你自己的测试引擎

Junit 目前提供了两种 TestEngine 实现:

第三方也可以通过在 junit-platform-engine 模块中实现接口并注册 引擎来提供他们自己的TestEngine。 目前Java的java.util.ServiceLoader机制支持引擎注册。 例如,junit-jupiter-engine模块将其org.junit.jupiter.engine.JupiterTestEngine注册到一个名为org.junit.platform.engine.TestEngine的文件中,该文件位于junit-jupiter-engineJAR包中的/META-INF/services目录。

📒 HierarchicalTestEngine 是一个边界的抽象基础实现(由junit-jupiter-engine使用),它只需要实现者为测试发现提供逻辑。 它实现了实现Node接口的TestDescriptors的执行,包括对并行执行的支持。

⚠️ junit-前缀是为JUnit Team的TestEngines保留的

JUnit 平台 Launcher强制规定只有JUnit团队发布的TestEngine实现可以使用junit-前缀作为其TestEngine ID。

  • 任何第三方TestEngine一旦声称是junit-jupiterjunit-vintage,JUnit平台则会抛出异常,并立即停止执行。
  • 任何第三方TestEngine使用junit-前缀作为其ID,JUnit平台则会记录警告消息,平台在后续版本将针对此类违规行为抛出异常。

7.1.4 插入你自己的测试执行监听器

除了以编程方式注册测试执行侦听器的公共 Launcher API方法之外,默认情况下,自定义的TestExecutionListener实现将在运行时通过Java的java.util.ServiceLoader机制发现,并自动注册到通过LauncherFactory创建的Launcher。 例如,一个实现了 TestExecutionListener 并声明在/META-INF/services/org.junit.platform.launcher.TestExecutionListener文件中的example.TestInfoPrinter类会被自动加载和注册。

7.1.5. 配置启动器

如果你需要对测试引擎和测试执行侦听器的自动检测和注册进行细粒度控制,你可以创建一个LauncherConfig实例并将其提供给LauncherFactory.create(LauncherConfig)方法即可。 通常情况下,LauncherConfig的实例是通过内置的流式构建器API创建的,如以下示例所示。

LauncherConfig launcherConfig = LauncherConfig.builder()
    .enableTestEngineAutoRegistration(false)
    .enableTestExecutionListenerAutoRegistration(false)
    .addTestEngines(new CustomTestEngine())
    .addTestExecutionListeners(new CustomTestExecutionListener())
    .build();

Launcher launcher = LauncherFactory.create(launcherConfig);

LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
    .selectors(selectPackage("com.example.mytests"))
    .build();

launcher.execute(request);

8. API演变

JUnit 5的主要目标之一是提高维护者发展演进JUnit的能力,尽管它在许多项目被使用。在JUnit 4中,起初作为内部构造而被添加的大量内容只能被外部扩展编写器和工具构建器使用。这就使得改变JUnit 4异常困难,甚至有时是不可能的。

这就是为什么JUnit 5为所有公开的接口、类和方法引入了一个明确的生命周期。

8.1. API 版本和状态

每个已发布的artifact都有一个版本号<major>.<minor>.<patch>,所有公开的接口、类和方法都使用了 @API Guardian项目中的 @API 注解进行标注。@API注解的status属性可以被赋予下面表格中的值。

状态描述
INTERNAL只能被JUnit自身使用,可能会被删除,但不事先另行通知。
DEPRECATED不应该再使用;可能会在下一个小版本中消失。
EXPERIMENTAL用于我们正在收集反馈的新的试验性功能。谨慎使用这个元素;它可能会在未来被提升为MAINTAINEDSTABLE,但也可能在没有事先通知的情况下被移除,即使在一个补丁中。
MAINTAINED用于至少 在当前主要版本的下一个次要版本中不会以反向不兼容的方式更改的功能。如果计划删除,则会首先将其降为DEPRECATED
STABLE用于在当前主版本(5. *)中不会以反向不兼容的方式更改的功能。

如果@API注解出现在某个类型上,则认为它也适用于该类型的所有公共成员。一个成员可以声明一个稳定性更低的status值。

8.2. 试验性API

下表列出了那些通过@API(status = EXPERIMENTAL)来标记为试验性的的API。使用这样的API时应该谨慎。

包名类型起始版本
org.junit.jupiter.apiAssertionsKt(类)5.1
org.junit.jupiter.api.conditionDisabledIf(注解)5.1
org.junit.jupiter.api.conditionEnabledIf(注解)5.1
org.junit.jupiter.api.extensionScriptEvaluationException(类)5.1
org.junit.jupiter.api.extensionTestInstanceFactory (接口)5.3
org.junit.jupiter.api.extensionTestInstanceFactoryContext (接口)5.3
org.junit.jupiter.api.extensionTestInstantiationException (类)5.3
org.junit.jupiter.api.parallelExecution (注解)5.3
org.junit.jupiter.api.parallelExecutionMode (枚举)5.3
org.junit.jupiter.api.parallelResourceAccessMode (枚举)5.3
org.junit.jupiter.api.parallelResourceLock (注解)5.3
org.junit.jupiter.api.parallelResourceLocks (注解)5.3
org.junit.jupiter.api.parallelResources (类)5.3
org.junit.jupiter.paramsParameterizedTest (注解)5.0
org.junit.jupiter.params.aggregatorAggregateWith (注解)5.2
org.junit.jupiter.params.aggregatorArgumentAccessException (类)5.2
org.junit.jupiter.params.aggregatorArgumentsAccessor (接口)5.2
org.junit.jupiter.params.aggregatorArgumentsAggregationException (类)5.2
org.junit.jupiter.params.aggregatorArgumentsAggregator (接口)5.2
org.junit.jupiter.params.converterArgumentConversionException (类)5.0
org.junit.jupiter.params.converterArgumentConverter (接口)5.0
org.junit.jupiter.params.converterConvertWith (接口)5.0
org.junit.jupiter.params.converterJavaTimeConversionPattern (接口)5.0
org.junit.jupiter.params.converterSimpleArgumentConverter (类)5.0
org.junit.jupiter.params.providerArguments (接口)5.0
org.junit.jupiter.params.providerArgumentsProvider (接口)5.0
org.junit.jupiter.params.providerArgumentsSource (接口)5.0
org.junit.jupiter.params.providerArgumentsSources (接口)5.0
org.junit.jupiter.params.providerCsvFileSource (接口)5.0
org.junit.jupiter.params.providerCsvParsingException (类)5.3
org.junit.jupiter.params.providerCsvSource (接口)5.0
org.junit.jupiter.params.providerEnumSource (接口)5.0
org.junit.jupiter.params.providerMethodSource (接口)5.0
org.junit.jupiter.params.providerValueSource (接口)5.0
org.junit.jupiter.params.supportAnnotationConsumer(接口)5.0
org.junit.platform.engine.support.configPrefixedConfigurationParameters (类)1.3
org.junit.platform.engine.support.hierarchicalDefaultParallelExecutionConfigurationStrategy (枚举)1.3
org.junit.platform.engine.support.hierarchicalExclusiveResource (类)1.3
org.junit.platform.engine.support.hierarchicalForkJoinPoolHierarchicalTestExecutorService (类)1.3
org.junit.platform.engine.support.hierarchicalHierarchicalTestExecutorService (接口)1.3
org.junit.platform.engine.support.hierarchicalExecutionMode (枚举)1.3
org.junit.platform.engine.support.hierarchicalParallelExecutionConfiguration (接口)1.3
org.junit.platform.engine.support.hierarchicalParallelExecutionConfigurationStrategy (接口)1.3
org.junit.platform.engine.support.hierarchicalResourceLock (接口)1.3
org.junit.platform.engine.support.hierarchicalSameThreadHierarchicalTestExecutorService (类)1.3

8.3. 弃用的API

下表列出了那些通过@API(status = DEPRECATED)来标记为弃用的API。由于这些API可能在接下来的版本中会被移除,所以你应该尽可能不要使用它们。

包名类型起始版本
org.junit.platform.engine.support.hierarchicalSingleTestExecutor (类)1.2
org.junit.platform.surefire.providerJUnitPlatformProvider (类)1.3

8.4. @API工具支持

@API Guardian 项目计划为使用 @API 注解的API的发布者和消费者提供工具支持。例如,工具支持可能会提供一种方法来检查是否按照@API注解声明来使用JUnit API。


9. 贡献者

9.1. Library

可以在GitHub上直接浏览 当前贡献者列表

9.2. 中文译者

袁慎建赵琪琪王亚鑫何僵乐


10. 发布记录

所有发布记录在 这里

版本 5.3.0
最后更新 2018-09-03

Posted by Yuan Shenjian

版权声明:自由转载•非商用•非衍生•保持署名 | Creative Commons BY-NC-ND 4.0

原文链接:https://sjyuan.cc/junit5/user-guide-cn/