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.1.1

  • 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.1.1

  • 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.1.1

  • Artifact ID:

junit-vintage-engine

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

2.1.4. 依赖

以上所有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. 标准测试类

一个标准的测试用例

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.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.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.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. 禁用测试

可以通过 @Disable 注解,或者通过 条件测试执行中讨论的注解之一,再或者通过自定义的 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 注解配置的脚本的评估,JUnit Jupiter提供了 启用或禁用 容器或测试的功能。脚本可以用JavaScript,Groovy或任何其他支持Java脚本API的脚本语言编写,由JSR 223定义。

⚠️ 通过 @EnabledIf@DisabledIf执行条件测试目前是一项试验性功能。有关详细信息,请参阅 实验性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以前的所有版本。

如果你希望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方法。

📒 在测试实例生命周期的上下文中,任何使用了@Test@RepeatedTest@ParameterizedTest@TestFactory或者@TestTemplate注解的方法都是测试 方法。

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的上下文之外被注册的。请参阅 重复测试示例
  • TestReporterParameterResolver:如果一个方法参数的类型是 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注册合适的 扩展 来明确地开启。

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import com.example.Person;
import com.example.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class MyMockitoTest {

    @BeforeEach
    void init(@Mock Person person) {
        when(person.getName()).thenReturn("Dilbert");
    }

    @Test
    void simpleTestWithInjectedMock(@Mock Person person) {
        assertEquals("Dilbert", person.getName());
    }

}

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的方法没有区别。此外,你必须声明至少一个参数源来给每次调用提供参数。

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

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

上面这个参数化测试使用@ValueSource注解来指定一个String数组作为参数源。执行上述方法时,每次调用会被分别报告。例如,ConsoleLauncher会打印类似下面的信息。

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

3.14.1. 必需的设置

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

3.14.2. 参数源

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

@ValueSource

@ValueSource是最简单来源之一。它允许你指定一个基本类型的数组(String、int、long或double),并且它只能为每次调用提供一个参数。

@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允许你引用测试类中的一个或多个工厂方法。这些工厂方法必须返回一个StreamIterableIterator或者参数数组。另外,它们不能接收任何参数。默认情况下,它们必须是static方法,除非测试类使用了@TestInstance(Lifecycle.PER_CLASS)注解。

如果你只需要一个参数,你可以返回一个参数类型的实例的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实例的集合或Stream,如下面代码所示。请注意,Arguments.of(Object ...)Arguments接口中定义的静态工厂方法。

@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"))
    );
}
@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.3. 参数转换

隐式转换

为了支持像@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.time.Instant"1970-01-01T00:00:00Z" → Instant.ofEpochMilli(0)
java.time.LocalDate"2017-03-14" → LocalDate.of(2017, 3, 14)
java.time.LocalDateTime"2017-03-14T12:34:56.789" → LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)
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.Year"2017" → Year.of(2017)
java.time.YearMonth"2017-03" → YearMonth.of(2017, 3)
java.time.ZonedDateTime"2017-03-14T12:34:56.789Z" → ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
显式转换

除了使用隐式转换参数,你还可以使用@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.4. 自定义显示名称

默认情况下,参数化测试调用的显示名称包含了该特定调用的索引和所有参数的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.5. 生命周期和互操作性

参数化测试的每次调用拥有跟普通@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方法必须返回一个DynamicNode实例的StreamCollectionIterableIteratorDynamicNode的两个可实例化子类是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 5.1.1中,动态测试必须始终由工厂方法创建;不过,在后续的发行版中,这可能会得到注册工具的补充。

⚠️ 动态测试目前是一个试验性功能。详细信息请参阅 试验性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()))
                ))
            )));
    }

}

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.1.1),你需要在类路径中引入相应版本的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.1.1")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1")
testRuntime("org.junit.vintage:junit-vintage-engine:5.1.1")
添加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.1.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.1.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <version>5.1.1</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

JUnit开发团队已经开发了一款非常基础的Gradle插件,它允许你运行被TestEngine(例如,JUnit3、JUnit4、JUnit Jupiter以及 Specsy 等)支持的任何种类的测试。关于插件的使用示例请参阅 junit5-gradle-consumer 项目中的build.gradle文件。

❗ ️️本地Gradle支持在这里
版本4.6 开始,Gradle为在JUnit平台上执行测试提供 native support。因此,junit-platform-gradle-plugin将在JUnit Platform 1.2中被弃用,并在版本1.3中停用。请切换到Gradle的标准test任务并使用JUnitPlatform()

启用JUnit Gradle插件

要使用JUnit Gradle插件,你首先要确保使用了Gradle 2.5或更高的版本,然后你可以按照下面的模板来配置项目中的build.gradle文件。

buildscript {
    repositories {
        mavenCentral()
        // The following is only necessary if you want to use SNAPSHOT releases.
        // maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
    dependencies {
        classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.2'
    }
}

apply plugin: 'org.junit.platform.gradle.plugin'
配置JUnit Gradle插件

一旦应用了JUnit Gradle插件,你就可以按照下面的方式进行配置。

junitPlatform {
    platformVersion '1.1.1' // optional, defaults to plugin version
    reportsDir file('build/test-results/junit-platform') // this is the default
    // enableStandardTestTask true
    // selectors (optional)
    // filters (optional)
    // logManager (optional)
}

JUnit Gradle插件在默认情况下会禁用标准的Gradle test任务,但可以通过enableStandardTestTask标志来启用。

配置选择器

默认情况下,插件会扫描项目中所有测试的输出目录。不过,你可以使用一个叫selectors的扩展元素来显式指定要执行哪些测试。

junitPlatform {
    // ...
    selectors {
        uris 'file:///foo.txt', 'http://example.com/'
        uri 'foo:resource'  
        files 'foo.txt', 'bar.csv'
        file 'qux.json'  
        directories 'foo/bar', 'bar/qux'
        directory 'qux/bar'  
        packages 'com.acme.foo', 'com.acme.bar'
        aPackage 'com.example.app'  
        classes 'com.acme.Foo', 'com.acme.Bar'
        aClass 'com.example.app.Application'  
        methods 'com.acme.Foo#a', 'com.acme.Foo#b'
        method 'com.example.app.Application#run(java.lang.String[])'  
        resources '/bar.csv', '/foo/input.json'
        resource '/com/acme/my.properties'  
    }
    // ...
}

① URIs
② 本地文件
③ 本地目录
④ 包
⑤ 类,全类名
⑥ 方法,全方法名(请参阅 DiscoverySelectors中的selectMethod(String)方法
⑦ 类路径资源

配置过滤器

你可以使用filters扩展来配置测试计划的过滤器。默认情况下,所有的引擎和标记都会被包含在测试计划中。但只有默认的includeClassNamePattern(^.*Tests?$)会被应用。你可以重写默认的匹配模式,例如下面示例。当你使用了多种匹配模式时,JUnit Platform会使用逻辑 或 将它们合并起来使用。

junitPlatform {
    // ...
    filters {
        engines {
            include 'junit-jupiter'
            // exclude 'junit-vintage'
        }
        tags {
            include 'fast', 'smoke & feature-a'
            // exclude 'slow', 'ci'
        }
        packages {
            include 'com.sample.included1', 'com.sample.included2'
            // exclude 'com.sample.excluded1', 'com.sample.excluded2'
        }
        includeClassNamePattern '.*Spec'
        includeClassNamePatterns '.*Test', '.*Tests'
    }
    // ...
}

如果你通过engines {include …​}engines {exclude …​}来提供一个测试引擎ID,那么JUnit Gradle插件将只运行你希望运行的那个测试引擎。同样,如果你通过tags {include …​}或者tags {exclude …​}提供一个标记标记表达式,JUnit Gradle插件将只运行相应标记的测试(例如,通过JUnit Jupiter测试的@Tag注解来过滤)。同理,关于包名,可以通过packages {include …​}或者packages {exclude …​}配置要包含或排除的包名。

配置参数

你可以使用configurationParameter或者configurationParameters DSL来设置配置参数,从而影响测试发现和执行。前者可以配置单独的配置参数,后者可以使用一个配置参数的map来一次性配置多个键-值对。所有的key和value都必须是String类型。

junitPlatform {
    // ...
    configurationParameter 'junit.jupiter.conditions.deactivate', '*'
    configurationParameters([
        'junit.jupiter.extensions.autodetection.enabled': 'true',
        'junit.jupiter.testinstance.lifecycle.default': 'per_class'
    ])
    // ...
}
配置测试引擎

为了让JUnit Gradle插件运行所有测试,类路径中必须存在一个TestEngine的实现。

要支持基于JUnit Jupiter的测试,你需要配置一个JUnit Jupiter API的 testCompile依赖以及JUnit Jupiter TestEngine实现的testRuntime依赖,具体配置如下。

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

只要你配置了一个JUnit 4的testCompile依赖以及JUnit Vintage TestEngine实现的testRuntime依赖,JUnit Gradle插件就可以运行基于JUnit 4的测试,具体配置如下。

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

JUnit使用java.util.logging包(a.k.a JUL)中的Java Logging API发出警告和调试信息。请参阅 LogManager 的官方文档以获取配置选项。

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

junitPlatform {
    logManager 'org.apache.logging.log4j.jul.LogManager'
}

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

使用JUnit Gradle插件

一旦应用并配置了JUnit Gradle插件,你就可以使用新的junitPlatformTest任务(在可用的Gralde task中会多出一个名为junitPlatformTest的Task)。

在命令行中调用gradlew junitPlatformTest(或者gradlew test)指令,项目中所有满足当前includeClassNamePattern(默认匹配^.*Tests?$)配置的测试会被执行。

junit5-gradle-consumer 项目中执行 junitPlatformTest任务会看到类似下面的输出。

:junitPlatformTest

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

BUILD SUCCESSFUL

如果测试失败,build会失败,并且会输出类似下面的信息。

:junitPlatformTest

Test failures (1):
  JUnit Jupiter:SecondTest:mySecondTest()
    MethodSource [className = 'com.example.project.SecondTest', methodName = 'mySecondTest', methodParameterTypes = '']
    => Exception: 2 is not equal to 1 ==> expected: <2> but was: <1>

Test run finished after 99 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         ]
[         2 tests successful      ]
[         1 tests failed          ]

:junitPlatformTest FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':junitPlatformTest'.
> Process 'command '/Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1

📒 当任何一个容器或测试失败时,退出值为1;否则,退出值为0.

⚠️ 当前JUnit Gradle插件的限制
任何通过JUnit Gradle插件运行的测试结果都不会包含在Gradle生成的标准测试报告中;但通常可以在持续集成服务器上汇总测试结果。详情请参阅插件的reportsDir属性。

4.2.2. Maven

JUnit团队已经为Maven Surefire开发了一个非常基础的provider,它允许你使用mvn test运行JUnit 4和JUnit Jupiter测试。junit5-maven-consumer 项目中的pom.xml文件演示了如何使用它,你可以以它作为一个起点。

⚠️ 由于Surefire2.20存在内存泄漏的漏洞,junit-platform-surefire-provider目前仅适用于Surefire 2.19.1。

...
<build>
    <plugins>
        ...
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19.1</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.1.1</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>
...
配置测试引擎

为了让Maven Surefire运行所有测试,必须将TestEngine实现添加到运行时类路径中。

要支持基于JUnit Jupiter的测试,你需要配置一个JUnit Jupiter API的test依赖,并将JUnit Jupiter TestEngine的实现添加到maven-surefire-plugin的依赖项中,如下所示。

...
<build>
    <plugins>
        ...
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19.1</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.1.1</version>
                </dependency>
                <dependency>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                    <version>5.1.1</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>
...
<dependencies>
    ...
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.1.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>
...

只要你配置了JUnit 4的test依赖,并将JUnit Vintage TestEngine的实现添加到maven-surefire-plugin的依赖项中,JUnit Platform Surefire Provider 就可以运行基于JUnit 4的测试。具体配置如下。

...
<build>
    <plugins>
        ...
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19.1</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.1.1</version>
                </dependency>
                ...
                <dependency>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                    <version>5.1.1</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>
...
<dependencies>
    ...
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>
...
运行单个测试类

JUnit Plaform Surefire Provider支持Maven Surefire插件所支持的测试JVM系统属性。例如,你只想要运行org.example.MyTest测试类中的测试方法,你可以在命令行执行mvn -Dtest = org.example.MyTest test。有关更多详细信息,请参阅 Maven Surefire Plugin 的文档。

按测试类名过滤

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

  • **/Test*.java

  • **/*Test.java

  • **/*TestCase.java

但是请注意,你可以通过在pom.xml文件中配置显式includeexclude规则来覆盖其默认行为。有关详细信息,请参阅 Inclusions and Exclusions of Tests 的文档。

按Tag过滤

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

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

配置参数

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

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

4.3. 控制台启动器

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

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

java -jar junit-platform-console-standalone-1.0.2.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退出,否则退出码为0.

4.3.1. Options

Option                                        Description
------                                        -----------
-h, --help                                    Display help information.
--disable-ansi-colors                         Disable ANSI colors in output (not
                                                supported by all terminals).
--details <[none,flat,tree,verbose]>          Select an output details mode for when
                                                tests are executed. Use one of: [none,
                                                flat, tree, verbose]. If 'none' is
                                                selected, then only the summary and test
                                                failures are shown. (default: tree)
--details-theme <[ascii,unicode]>             Select an output details tree theme for
                                                when tests are executed. Use one of:
                                                [ascii, unicode] (default: unicode)
--class-path, --classpath, --cp <Path:        Provide additional classpath entries --
  path1:path2:...>                              for example, for adding engines and
                                                their dependencies. This option can be
                                                repeated.
--reports-dir <Path>                          Enable report output into a specified
                                                local directory (will be created if it
                                                does not exist).
--scan-class-path, --scan-classpath [Path:    Scan all directories on the classpath or
  path1:path2:...]                              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 <String>                    Select a file for test discovery. This
                                                option can be repeated.
-d, --select-directory <String>               Select a directory for test discovery.
                                                This option can be repeated.
-p, --select-package <String>                 Select a package for test discovery. This
                                                option can be repeated.
-c, --select-class <String>                   Select a class for test discovery. This
                                                option can be repeated.
-m, --select-method <String>                  Select a method for test discovery. This
                                                option can be repeated.
-r, --select-resource <String>                Select a classpath resource for test
                                                discovery. This option can be repeated.
-n, --include-classname <String>              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 end with
                                                "Test" or "Tests". When this option is
                                                repeated, all patterns will be combined
                                                using OR semantics. (default: ^.*Tests?$)
-N, --exclude-classname <String>              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 <String>                    Provide a package to be included in the
                                                test run. This option can be repeated.
--exclude-package <String>                    Provide a package to be excluded from the
                                                test run. This option can be repeated.
-t, --include-tag <String>                    Provide a tag to be included in the test
                                                run. This option can be repeated.
-T, --exclude-tag <String>                    Provide a tag to be excluded from the test
                                                run. This option can be repeated.
-e, --include-engine <String>                 Provide the ID of an engine to be included
                                                in the test run. This option can be
                                                repeated.
-E, --exclude-engine <String>                 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.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包及其子包下的测试。默认情况下,它只包含类名符合正则表达式^.*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. 标记表达式

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

Table 1. Operators (in descending order of precedence

OperatorMeaningAssociativity
!notright
&andleft
|orleft

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

Tag ExpressionSelection
fooall tests for foor
bar | bazall tests for bar plus all tests for baz
bar & bazall tests foro the interaction between bar and baz
foo & !end-to-endall tests for foo, but not the end-to-end tests
(micro | integration) & (foo | baz)all micro or integration tests for foo or baz

5. 扩展模型

5.1. 概述

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

5.2. 注册扩展

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

5.2.1. 声明式扩展注册

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

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

@ExtendWith(MockitoExtension.class)
@Test
void mockTest() {
    // ...
}

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

@ExtendWith(MockitoExtension.class)
class MockTests {
    // ...
}

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

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

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

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

📒 扩展注册顺序
通过@ExtendWith以声明方式注册的扩展将按照它们在源代码中声明的顺序执行。例如,MyTestsV1MyTestsV2中的测试执行将按照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());
    }
}
实例字段

如果@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@Disable 的源码。

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. 测试实例后处理

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

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

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

5.5. 参数解析

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.6. 测试生命周期回调

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

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

5.6.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.7. 异常处理

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.8. 为测试模板提供调用上下文

当至少有一个 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.9. 在扩展中保持状态

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

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

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

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

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

用户代码和扩展代码

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

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

步骤接口/注解描述
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目录。

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

除了以编程方式来注册测试执行监听器的公共 Launcher API方法之外,在运行时由Java的java.util.ServiceLoader发现的自定义 TestExecutionListener 实现会被自动注册到DefaultLauncher。 例如,一个实现了 TestExecutionListener 并声明在/META-INF/services/org.junit.platform.launcher.TestExecutionListener文件中的example.TestInfoPrinter类会被自动加载和注册。


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当前被指定为试验性的(通过@API(status = EXPERIMENTAL))。使用这样的API时应该谨慎。

包名类名类型
org.junit.jupiter.apiAssertionsKt
org.junit.jupiter.apiDynamicContainer
org.junit.jupiter.apiDynamicNode
org.junit.jupiter.apiDynamicTest
org.junit.jupiter.apiTestFactory注解
org.junit.jupiter.api.conditionDisabledIf注解
org.junit.jupiter.api.conditionEnabledIf注解
org.junit.jupiter.api.extensionScriptEvaluationException
org.junit.jupiter.migrationsupport.rulesEnableRuleMigrationSupport注解
org.junit.jupiter.migrationsupport.rulesExpectedExceptionSupport
org.junit.jupiter.migrationsupport.rulesExternalResourceSupport
org.junit.jupiter.migrationsupport.rulesVerifierSupport
org.junit.jupiter.paramsParameterizedTest注解
org.junit.jupiter.params.converterArgumentConversionException
org.junit.jupiter.params.converterArgumentConverter接口
org.junit.jupiter.params.converterConvertWith注解
org.junit.jupiter.params.converterJavaTimeConversionPattern注解
org.junit.jupiter.params.converterSimpleArgumentConverter
org.junit.jupiter.params.providerArguments接口
org.junit.jupiter.params.providerArgumentsProvider接口
org.junit.jupiter.params.providerArgumentsSource注解
org.junit.jupiter.params.providerArgumentsSources注解
org.junit.jupiter.params.providerCsvFileSource注解
org.junit.jupiter.params.providerCsvSource注解
org.junit.jupiter.params.providerEnumSource注解
org.junit.jupiter.params.providerMethodSource注解
org.junit.jupiter.params.providerValueSource注解
org.junit.jupiter.params.supportAnnotationConsumer接口
org.junit.platform.gradle.pluginEnginesExtension
org.junit.platform.gradle.pluginFiltersExtension
org.junit.platform.gradle.pluginJUnitPlatformExtension
org.junit.platform.gradle.pluginJUnitPlatformPlugin
org.junit.platform.gradle.pluginPackagesExtension
org.junit.platform.gradle.pluginSelectorsExtension
org.junit.platform.gradle.pluginTagsExtension
org.junit.platform.surefire.providerJUnitPlatformProvider

8.3. @API工具支持

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


9. 贡献者

Library

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

中文译者

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


10. 发布记录

所有发布记录在 这里

版本 5.1.1
最后更新 2018-04-08

Posted by Yuan Shenjian

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

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