1.概述

JUnit是Java生态系统中最受欢迎的单元测试框架之一。

JUnit 5版本包含许多令人兴奋的创新,其目标是支持Java 8及更高版本中的新功能,以及支持许多不同类型的测试。

2. Maven依赖

设置Junit 5 很简单,我们只需要在pom.xml中添加以下依赖项:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.1.0</version>
    <scope>test</scope>
</dependency>

需要注意的是Junit 5 需要Java 8及以上。

现在Eclipse和IntelliJ里都支持JUnit 5。

3 JUnit 5 的组成

JUnit 5由来自三个不同子项目的几个不同模块组成:

3.1 JUnit Platform

JUnit平台负责在JVM上启动测试框架。

它定义了JUnit与其客户端之间稳定而强大的接口,比如构建工具。

目标是如何让客户轻松地与JUnit集成,以发现和执行测试。

它还定义了TestEngine API,用于开发在JUnit平台上运行的测试框架。

通过这种方式,可以实现自定义TestEngine将第三方测试库直接插入JUnit。

3.2 JUnit Jupiter

这个模块包括了用于在JUnit 5中编写测试的新编程和扩展模型。

与JUnit 4相比,新的注释有:

  • @TestFactory - 表示一个方法,它是动态测试的测试工厂
  • @DisplayName - 定义测试类或测试方法的自定义显示名称
  • @Nested - 表示带注释的类是嵌套的非静态测试类
  • @Tag - 声明用于过滤测试的标签
  • @ExtendWith - 用于注册自定义扩展
  • @BeforeEach -表示在每个测试方法之前执行带注释的方法(之前为@Before
  • @AfterEach - 表示在每个测试方法之后将执行带注释的方法(之前为*@After*)
  • @BeforeAll - 表示将在当前类中的所有测试方法之前执行带注释的方法(以前为*@BeforeClass*)
  • @AfterAll - 表示将在当前类中的所有测试方法之后执行带注释的方法(以前为*@AfterClass*)
  • @Disable - 用于禁用测试类或方法(以前为*@Ignore*)

3.3 JUnit Vintage

这个模块是为支持在JUnit 5平台上运行基于JUnit 3和JUnit 4的测试。

4. 常用注解

为了讨论新的注释,我们将该部分分为以下几组:

  • 在测试之前
  • 在测试期间
  • 测试之后:

4.1 @BeforeAll和@BeforeEach

    @BeforeAll
    static void setup() {
        log.info("@BeforeAll - 只执行一次,在所有测试开始之前");
    }

    @BeforeEach
    void init() {
        log.info("@BeforeEach - 执行多次,每次执行测试之前都要执行");
    }

使用*@BeforeAll*注释的方法需要是静态的,否则代码将无法编译。

4.2 @DisplayName和@Disabled

这两个比较简单,用法和他们名字的含义一样。

    @DisplayName("这是一个什么测试")
    @Test
    void tes_Xxx() {
        log.info("Success");
    }

    @Test
    @Disabled("比如这个测试方法还没有实现或过时")
    void test_Something() {
    }

4.3。@AfterEach和@AfterAll

    @AfterEach
    void tearDown() {
        log.info("@AfterEach - 执行多次,每次执行测试之后都要执行");
    }

    @AfterAll
    static void done() {
        log.info("@AfterAll - 只执行一次,在所有测试结束之后");
    }

使用*@AfterAll的*方法也需要是静态方法。

5.断言和假设

JUnit 5试图充分利用Java 8的新特性,特别是lambda表达式。

5.1 断言

断言已移至org.junit.jupiter.api.Assertions并改变有点大。在Junit 5中我们可以在断言中使用lambdas:

    @Test
    void test_lambdaExpressions() {
        assertTrue(Stream.of(1, 2, 3)
                .mapToInt(i -> i)
                .sum() > 7, () -> "和应该大于7");
    }

如果我们执行的话,打印如下

7月 18, 2019 9:05:53 下午 com.ripjava.junit5.basic.BasicAnnotations setup
信息: @BeforeAll - 只执行一次,在所有测试开始之前
7月 18, 2019 9:05:53 下午 com.ripjava.junit5.basic.BasicAnnotations init
信息: @BeforeEach - 执行多次,每次执行测试之前都要执行
7月 18, 2019 9:05:53 下午 com.ripjava.junit5.basic.BasicAnnotations tearDown
信息: @AfterEach - 执行多次,每次执行测试之后都要执行

7月 18, 2019 9:05:53 下午 com.ripjava.junit5.basic.BasicAnnotations done
信息: @AfterAll - 只执行一次,在所有测试结束之后

org.opentest4j.AssertionFailedError: 和应该大于7 ==> expected: <true> but was: <false>

虽然上面的例子虽然微不足道的,但是对断言消息使用lambda表达式的一个优点是它被懒惰执行,如果消息构造很昂贵,这可以节省时间和资源。

而且还可以使用assertAll()对断言进行分组,该断言将使用MultipleFailuresError报告组内任何失败的断言:

    @Test
    void test_groupAssertions() {
        int[] numbers = {0, 1, 2, 3, 4};

        assertAll("numbers",
                () -> assertEquals(numbers[0], 0),
                () -> assertEquals(numbers[3], 3),
                () -> assertEquals(numbers[4], 4));
    }

这意味着在制作更复杂的断言会更安全,因为能够确定任何失败断言的确切位置。

5.2 假设

假设仅在满足某些条件时用于运行测试。

通常用于测试正常运行所需的外部条件,但这些条件与正在测试的内容无直接关系。

可以使用assumeTrue()assumeFalse()assumeThat()声明一个假设

    @Test
    void test_trueAssumption() {
        assumeTrue(5 > 1);
        assertEquals(5 + 2, 7);
    }

    @Test
    void test_falseAssumption() {
        assumeFalse(5 < 1);
        assertEquals(5 + 2, 7);
    }

    @Test
    void test_assumptionThat() {
        String someString = "Just a string";
        assumingThat(
                someString.equals("Just a string"),
                () -> assertEquals(2 + 2, 4)
        );
    }

如果假设失败,则抛出TestAbortedException并简单地跳过测试。

和断言一样,假设也可以使用lambda表达式。

6. 异常测试

在JUnit 5中有两种异常测试方法。它们都可以使用*assertThrows()*方法实现:

    @Test
    void shouldThrowException() {
        Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
            throw new UnsupportedOperationException("Not supported");
        });
        assertEquals(exception.getMessage(), "Not supported");
    }

    @Test
    void assertThrowsException() {
        String str = null;
        assertThrows(IllegalArgumentException.class, () -> {
            Integer.valueOf(str);
        });
    }

第一个示例用于验证抛出异常的更多细节。

第二个示例仅用于验证异常类型。

7.动态测试

动态测试允许声明和运行在运行时生成的测试用例。

与在编译时定义固定数量的测试用例的静态测试相反,动态测试允许我们在运行时动态定义测试用例。

动态测试可以通过使用*@TestFactory注释的工厂方法生成。*我们来看看代码示例:

    @TestFactory
    public Stream<DynamicTest> translateDynamicTestsFromStream() {
        List<String> in = new ArrayList<>(Arrays.asList("Hello", "Yes", "No"));
        List<String> out = new ArrayList<>(Arrays.asList("こにちは", "はい", "いいえ"));
        return in.stream()
                .map(word ->
                        DynamicTest.dynamicTest("Test translate " + word, () -> {
                            int id = in.indexOf(word);
                            assertEquals(out.get(id), translate(word));
                        })
                );
    }

    private String translate(String word) {
        if ("Hello".equalsIgnoreCase(word)) {
            return "こにちは";
        } else if ("Yes".equalsIgnoreCase(word)) {
            return "はい";
        } else if ("No".equalsIgnoreCase(word)) {
            return "いいえ";
        }
        return "Error";
    }

我们使用两个ArrayList来翻译单词,分别命名为inout

工厂方法必须返回StreamCollectionIterableIterator

在我们的例子中,我们选择Java 8 Stream。

请注意,@ TestFactory方法不能是私有或静态的。测试的数量是动态的,上面的示例取决于ArrayList的大小。

8.结论

这篇文章简单的介绍了JUnit 5。

我们可以发现JUnit 5的体系结构发生了很大变化,涉及平台启动器,与构建工具,IDE,和单元测试框架的集成等。此外,JUnit 5与Java 8更加集成,特别是与Lambdas和Stream概念相关。

最后,和往常一样,代码可以在Github上找到。