Junit5

目录

Junit5 介绍

什么是 junit5 ?

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform::Junit Platform 是在 JVM 上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也都可以接入。
  • JUnit Jupiter:JUnit Jupiter 提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部包含了一个测试引擎,用于在 Junit Platform 上运行。
  • JUnit Vintage:由于 JUint 已经发展多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎。

通过上述的介绍,不知道有没有发现 JUint5 似乎已经不再满足于安安静静做一个单元测试框架了,它的野心很大,想通过接入不同测试引擎,来支持各类测试框架的使用,成为一个基于 JVM 测试框架的基础平台。因此它也采用了分层的架构,分为了平台层、引擎层、框架层。下图可以很清晰地体现出来:

Junit5

  • 最核心的就是平台层:IDE 和构建工具都是作为客户端和这个平台层交互,以达到在项目中运行测试的目的。TestEngine 的实现在平台层中用于发现和运行测试,并且输出测试报告,并通过平台层返回给客户端。

  • 核心关注点是扩展能力:不仅仅只是存在于测试类级别,在整个测试平台级别,都提供了足够的扩展能力。任何一个框架都可以在JUnit平台上运行他自己的测试,只需要实现框架本身对 TestEngine 的接口,任何测试框架都可以在 JUnit Platform 上运行,这代表着 JUnit5 将会有着很强的拓展性。只需要一点点工作,通过这一个扩展点,框架就能得到所有 IDE 和构建工具在测试上的支持。这对于新框架来说绝对是好事,在测试和构建这块的门槛更低。如 JUnit Vintage 就是一个 TestEngine 实现,用于执行 JUnit4 的测试。

  • 这些对于一个开发者来说意味着什么呢?这意味着一个测试框架和 JVM 开发市场上所有主流的工具集成的时候,你能更容易地说服你的经理、开发 leader、任何项阻碍你引入这个测试框架的人。

Junit5 新特性

JUnit5 更像是 JUnit4 的一个超集,他提供了非常多的增强:

  • 解决了前序版本的问题:如更强大的断言功能
  • 嵌套测试类:不仅仅是 BDD(Behavior Driven Development)
  • 动态测试:在运行时生成测试用例
  • 扩展测试:允许你重新定义测试的行为,允许你使用插件的形式重用你的测试类

迁移指南:

JUnit 平台可以通过 Jupiter 引擎来运行 JUnit 5 测试,通过 Vintage 引擎来运行 JUnit 3 和 JUnit 4 测试。因此,已有的 JUnit 3 和 4 的测试不需要任何修改就可以直接在 JUnit 平台上运行。只需要确保 Vintage 引擎的 jar 包出现在 classpath 中,JUnit 平台会自动发现并使用该引擎来运行 JUnit 3 和 4 测试。

开发人员可以按照自己的项目安排来规划迁移到 JUnit 5 的进度。可以保持已有的 JUnit 3 和 4 的测试用例不变,而新增加的测试用例则使用 JUnit 5。

在进行迁移的时候需要注意如下的变化:

  • 注解在 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、@Rule 和 @ClassRule 替换成 @ExtendWith

Junit5 注解

注解 说明
@Test 表示方法是测试方法(与 JUnit4 的 @Test 不同,它的职责非常单一,不能声明任何属性,拓展的测试将会由 Jupiter 提供额外注解)
@ParameterizedTest 表示方法是参数化测试
@RepeatedTest 表示方法可重复执行
@DisplayName 为测试类或者测试方法设置展示名称
@BeforeEach 表示在每个测试方法之前执行
@AfterEach 表示在每个测试方法之后执行
@BeforeAll 只执行一次,执行时机是在所有测试方法和 @BeforeEach 注解方法之前
@AfterAll 只执行一次,执行时机是在所有测试方法和 @AfterEach 注解方法之后
@Tag 表示单元测试类别。类似于 JUnit4 中的 @Categories
@Disabled 表示测试类或测试方法不执行。类似于 JUnit4 中的 @Ignore
@Timeout 表示测试方法运行如果超过了指定时间将会返回错误
@ExtendWith 为测试类或测试方法提供扩展类引用
  • JUnit5 不再需要手动将测试类与测试方法为 public,包可见的访问级别就足够了。
  • 因为框架会为每个测试类创建一个单独的实例,且在 @BeforeAll/@AfterAll 方法执行时,尚无任何测试实例诞生。因此,这两个方法必须定义为静态方法。

示例:

import org.junit.jupiter.api.*;

class StandardTests {

    @BeforeAll
    static void initAll() {
        System.out.println("BeforeAll");
    }

    @BeforeEach
    void init() {
        System.out.println("BeforeEach");
    }

    @Test
    void test1() {
        System.out.println("test1");
    }

    @Test
    void test2() {
        System.out.println("test2");
    }

    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
        System.out.println("skippedTest");
    }

    @AfterEach
    void tearDown() {
        System.out.println("AfterEach");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("AfterAll");
    }

}

执行结果:

BeforeAll
BeforeEach
test1
AfterEach
BeforeEach
test2
AfterEach

for demonstration purposes
AfterAll

断言

JUnit5 使用了新的断言类:org.junit.jupiter.api.Assertions。相比之前的 Assert 断言类多了许多新的功能,并且大量方法支持 Java8 的 Lambda 表达式。

以下为两个与 JUnit4 不太一样的断言方式。

异常断言

JUnit5 提供了一种新的异常断言方式 Assertions.assertThrows(),配合函数式编程就可以进行使用。

我们先来考虑一下下面这个 JUnit4 测试:

@Test(expected = IllegalArgumentException.class)
public void shouldThrowException() throws Exception {
    Task task = buildTask();
    LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
    task.execute(oneHourAgo);
}

想象我们运行这个测试,如果传入到 execute() 方法中的参数是一个过去的时间,会正常抛出一个 IllegalArgumentException 异常。这种情况下,测试会运行通过。

但是如果在 buildTask() 方法中抛出了一个其他类型的异常呢?测试会正常执行,并且会提示你得到的异常和期望异常不匹配。这里问题就出来了,我们只是希望测试是在指定位置得到指定的异常,而不是在整个测试体中出现的异常都作为对比异常。

为此,在 JUnit5 中,提供了一个 assertThrows() 方法,可以非常轻松地处理这个问题:

@Test
void shouldThrowException() throws Exception {
    Task task = buildTask();
    LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
    assertThrows(IllegalArgumentException.class,
                 () -> task.execute(oneHourAgo));
}

超时断言

同样的,Junit5 还提供了 Assertions.assertTimeout() 方法,为测试方法的指定位置,设置超时测试。

在这种情况下,就不会担心测试的 setup 阶段对代码执行时间的影响,你可以指定只去衡量某一段代码的执行时间。

另外还提供了一个选项:当出现超时的时候,是选择停止执行(即断言失败)还是继续当前测试(以衡量代码执行的真实完整时间)。

@Test
void shouldTimeout() throws Exception {
    ExpensiveService service = setupService();
    assertTimeout(ofSeconds(3), () -> {
        service.expensiveMethod();
    });
}

综合示例

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

}

参数化测试

在 JUnit4 中,如果想要实现参数化测试(使用不同的参数来测试相同的一个方法),只能使用测试类中的字段来实现。而在 JUnit5 中,提供了参数化测试来实现这个需求。不同的参数值可以直接和一个测试方法关联,并且允许直接在一个测试类中提供不同的参数值直接参与测试,这些在 JUnit4 中都是无法实现的。

测试参数可以通过一组 CSV 格式的字符串、外部的 CSV 文件、枚举、工厂方法,或者指定的提供类来提供。CSV 中的字符串类型的值还可以自动地转化为指定的类型,并且可以完成自己的类型转换器,如将 String 转成你希望的任何指定类型。

指定入参

利用 @ValueSource 等注解指定入参,我们可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类、String、Class 类型。
  • @NullSource: 表示为参数化测试提供一个 null 入参。
  • @EnumSource: 表示为参数化测试提供一个枚举入参。

示例:

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.platform.commons.util.StringUtils;

class ParameterTest {

    @ParameterizedTest
    @ValueSource(strings = {"one", "two", "three"})
    @DisplayName("参数化测试")
    public void parameterizedTest1(String string) {
        System.out.println(string);
        Assertions.assertTrue(StringUtils.isNotBlank(string));
    }

}

外部入参

Junit5 支持外部的各类入参,如 CSV、YML、JSON 文件、方法的返回值,也都可以作为入参。只需要实现 ArgumentsProvider 接口,任何外部文件都可以作为它的入参。

  • @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参。
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意:方法返回值需要是一个流)。

示例:

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

class ParameterTest {

    /**
     * csv文件内容:
     * shawn,24
     * uzi,50
     */
    @ParameterizedTest
    @CsvFileSource(resources="/test.csv")  // 指定csv文件位置
    @DisplayName("参数化测试-从csv文件获取")
    public void parameterizedTestWithCsv(String name, Integer age) {
        System.out.println("name:" + name + ", age:" + age);
        Assertions.assertNotNull(name);
        Assertions.assertNotNull(age);
    }

    @ParameterizedTest
    @MethodSource("getData")  // 指定方法名
    @DisplayName("参数化测试-从方法获取")
    public void parameterizedTestWithMethod(String name) {
        System.out.println(name);
        Assertions.assertNotNull(name);
    }

    static Stream<String> getData() {
        return Stream.of("apple", "banana");
    }

}

嵌套测试

JUnit5 提供了嵌套测试用于更好表示各个单元测试类之间的关系。平时我们写单元测试时一般都是一个类对应一个单元测试类,不过有些互相之间有业务关系的类,他们的单元测试完全是可以写在一起。因此,使用内嵌的方式表示,能够减少测试类的数量,防止类爆炸。

JUnit5 提供了 @Nested 注解,能够以静态成员内部类的形式对测试用例类进行逻辑分组。

示例:

import org.junit.jupiter.api.*;

class NestedTest {

    @BeforeEach
    void init() {
        System.out.println("init");
    }

    @Test
    @DisplayName("Nested")
    void test() {
        System.out.println("test");
    }

    @Nested
    @DisplayName("Nested2")
    class Nested2 {

        @BeforeEach
        void Nested2_init() {
            System.out.println("Nested2_init");
        }

        @Test
        void Nested2_test1() {
            System.out.println("Nested2_test1");
        }

        @Test
        void Nested2_test2() {
            System.out.println("Nested2_test2");
        }

        @Nested
        @DisplayName("Nested3")
        class Nested3 {

            @BeforeEach
            void Nested3_init() {
                System.out.println("Nested3_init");
            }

            @Test
            void Nested3_test1() {
                System.out.println("Nested3_test1");
            }

            @Test
            void Nested3_test2() {
                System.out.println("Nested3_test2");
            }
        }
    }

}

执行结果:

init
test
init
Nested2_init
Nested2_test1
init
Nested2_init
Nested2_test2
init
Nested2_init
Nested3_init
Nested3_test1
init
Nested2_init
Nested3_init
Nested3_test2

重复测试

重复运行单元测试可以更加保证测试的准确性,规避一些随机性带来的测试问题。

@RepeatedTest(10)  // 表示重复执行10次
@DisplayName("重复测试")
public void testRepeated() {
    Assertions.assertTrue(1==1);
}
上一篇:Junit5快速入门指南-4


下一篇:leetcode 85 最大矩形(动态规划做法)