JUnit 5

806 阅读8分钟

什么是 JUnit

JUnitJava单元测试框架, 起源于 1997 年,最初版本是由两位编程大师 Kent Beck 和 Erich Gamma 的一次飞机之旅上完成的,由于当时 Java 测试过程中缺乏成熟的工具,两人在飞机上就合作设计实现了 JUnit 雏形, 经过多个版本迭代演进,现已成为 java 单元测试的事实标准

JUnit 隶属于 xUnitxUnit 是一套基于测试驱动开发的测试框架,包括:

  • PythonUnit : Python 的单元测试框架
  • CppUnit : C++ 的单元测试框架
  • JUnit : java 的单元测试框架

JUnit 5

JUnit 5 是 JUnit 单元测试框架的一次重大升级,首先需要 Java 8 以上的运行环境,虽然在旧版本 JDK 也能编译运行,但要完全使用 JUnit 5 功能, JDK 8 环境是必不可少的。

除此之外,JUnit 5 与以前版本的 JUnit 不同,拆分成由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform : 用于 JVM 上启动测试框架的基础服务,提供命令行,IDE 和构建工具等方式执行测试的支持。JUnit Platform不仅支持 Junit 自制的测试引擎,其他测试引擎也都可以接入。
  • JUnit Jupiter : 包含 JUnit 5 新的编程模型和扩展模型,主要就是用于编写测试代码和扩展代码。
  • JUnit Vintage : 用于在 JUnit 5 中兼容运行 JUnit3.x 和 JUnit4.x 的测试用例。

JUint5 已经不再满足于做一个单元测试框架了,它的野心很大,想通过接入不同测试引擎,来支持各类测试框架的使用,成为一个单元测试的平台。因此它也采用了分层的架构,分成了平台层,引擎层,框架层。

junit 5 架构及依赖

JUnit 5 的架构如下

只要实现了 JUnit 的测试引擎接口,任何测试框架都可以在 JUnit Platform 上运行,这代表着 JUnit5 将会有着很强的拓展性

JUnit 5 依赖关系如下

JUnit 5 新特性

  • 提供全新的断言和测试注解
  • 支持测试类内嵌
  • 更丰富的测试方式:支持动态测试,重复测试,参数化测试等
  • 实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖
  • 提供对 Java 8 的支持,如 Lambda 表达式,Sream API 等。

引入依赖

<!-- JUnit 5 API -->
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <version>5.x.x</version>
  <scope>test</scope>
</dependency>

<!-- 旧的 JUnit API,如果需要兼容JUnit 4等旧的api 则需要引入,新项目不需要 -->
<dependency>
  <groupId>org.junit.vintage</groupId>
  <artifactId>junit-vintage-engine</artifactId>
  <version>5.x.x</version>
  <scope>test</scope>
</dependency>

JUnit 5 中的注解

JUnit 5JUnit 4 中的注解进行了变更和补充 ,以下是对照表

JUnit 4JUnit 5注解说明
@Test@Test声明一个测试方法
@BeforeClass@BeforeAll在当前类 所有的测试方法执行之前,执行
@AfterClass@AfterAll在当前类 所有的测试方法执行之后,执行
@Before@BeforeEach在每个测试方法执行之前,执行
@After@AfterEach在每个测试方法执行之后,执行
@Ignore@Disabled禁用一个测试方法或测试类
@Category@Tag给测试类或测试方法打标签
NA@DisplayName设置测试类或测试方法的展示名称
NA@RepeatedTest重复性测试
NA@ValueSource 、@CsvSource 等参数化测试
NA@Nested内嵌其他测试类
NA@TestFactory使用 TestFactory 进行动态测试

常用注解示例

import org.junit.jupiter.api.*;

@DisplayName("测试用例")
class MyTest {

    @BeforeAll
    public static void init() {
        System.out.println("初始化数据");
    }

    @AfterAll
    public static void cleanup() {
        System.out.println("清理数据");
    }

    @BeforeEach
    public void tearup() {
        System.out.println("当前测试方法开始");
    }

    @AfterEach
    public void tearDown() {
        System.out.println("当前测试方法结束");
    }

    @DisplayName("测试1")
    @Test
    void testFirstTest() {
        System.out.println("测试1开始测试");
    }

    @DisplayName("测试2")
    @Test
    void testSecondTest() {
        System.out.println("测试2开始测试");
    }

    @Disabled
    @DisplayName("测试3")
    @Test
    void testThirdTest() {
        System.out.println("测试3开始测试");
    }
    
}

执行结果及各注解的作用如下图

注意 @BeforeAll@AfterAll 注解只能修饰静态方法

不同于 JUnit 4 , JUnit 5api 均来自 org.junit.jupiter.api

重复性测试

在 JUnit 5 里新增了对测试方法设置运行次数的支持,允许让测试方法进行重复运行。当要运行一个测试方法 N 次时,可以使用 @RepeatedTest 标记它

import org.junit.jupiter.api.*;

@DisplayName("测试类")
public class MyTest {

    @DisplayName("重复测试")
    @RepeatedTest(value = 3)
    public void test() {
        System.out.println("执行测试");
    }
}

我们还可以对重复运行的测试方法名称进行修改,利用 @RepeatedTest 提供的内置变量,以占位符方式在其 name 属性上使用

@DisplayName("自定义名称重复测试")
@RepeatedTest(value = 3, name = "{displayName} 第 {currentRepetition} 次")
public void test() {
	System.out.println("执行测试");
}

@RepeatedTest 注解内置变量如下

  • currentRepetition 变量表示已经重复的次数
  • totalRepetitions 变量表示总共要重复的次数
  • displayName 变量表示测试方法显示名称 我们可以使用这些内置的变量来重新定义方法重复运行时的名称

嵌套测试

当我们编写的类和代码逐渐增多,随之而来的需要测试的对应测试类也会越来越多。为了解决测试类数量爆炸的问题,JUnit 5 提供了 @Nested 注解,能够以静态内部成员类的形式对测试用例类进行逻辑分组。 并且每个静态内部类都可以有自己的生命周期方法, 这些方法将按从外到内层次顺序执行。 此外,嵌套的类也可以用 @DisplayName 标记,这样我们就可以使用正确的测试名称。

import org.junit.jupiter.api.*;

@DisplayName("内嵌测试类")
public class MyTest {

    @Nested
    @DisplayName("内嵌测试类A")
    class FirstNestTest {
        @DisplayName("测试方法A1")
        @Test
        void test() {
            System.out.println("测试方法A1");
        }
    }

    @Nested
    @DisplayName("内嵌测试类B")
    class SecondNestTest {
        @DisplayName("测试方法B1")
        @Test
        void test() {
            System.out.println("测试方法B1");
        }

        @DisplayName("测试方法B2")
        @Test
        void test2() {
            System.out.println("测试方法B2");
        }
    }
}

参数化测试

要使用 JUnit 5 参数化测试功能,除了 junit-jupiter-engine 基础依赖之外,还需要 junit-jupiter-params 依赖

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-params</artifactId>
  <version>5.x.x</version>
  <scope>test</scope>
</dependency>

参数化测试使用 @ParameterizedTest 注解,替代 @Test 注解

@ValueSource

@ValueSource 是 JUnit 5 提供的最简单的数据参数源,支持 Java 的八大基本类型和字符串,Class,使用时赋值给注解上对应类型属性,以数组方式传递

@CsvSource

@CsvSource 注解可以支持 CSV 格式的数据,默认分隔符为逗号,也可以通过 delimiter 属性指定

@CsvFileSource

除了像 @CsvSource 直接把数据写在代码里,JUnit 5 还支持使用 @CsvFileSource 注解,指定外部 csv 文件,指定的资源文件路径时要以 / 开始,寻找当前测试资源目录下文件

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

@DisplayName("测试类")
public class MyTest {

    @ParameterizedTest
    @CsvFileSource(resources = "/test.csv",  delimiter = '@' )
    void testDataFromCsv(long id, String name) {
        System.out.printf("id: %d, name: %s", id, name);
    }
}

@EnumSource

JUnit 5 还支持枚举类型的数据源,使用 @EnumSource 注解可以指定一个枚举类,另外 @EnumSource 还提供了多种挑选枚举值的模式,比如下面例子中排除 DAYS、HOURS 两个枚举值

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import java.util.EnumSet;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE;

@DisplayName("测试类")
public class MyTest {
    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
    void testWithEnumSourceExclude(TimeUnit timeUnit) {
        assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
    }
}

除了排除模式,还可以使用正则匹配模式

@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
void testWithEnumSourceExclude(TimeUnit timeUnit) {
    assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}

@MethodSource

@MethodSource 注解可以指定一个返回的 Stream 、 Array 、 可迭代对象的方法作为数据源。 需要注意的是该方法必须是静态的,并且不能接受任何参数

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@DisplayName("测试类")
public class MyTest {
    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithSimpleMethodSource(String argument) {
        assertNotNull(argument);
    }

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

新的断言

JUnit 5JUnit 4 的基础上增加了些新的断言

超时断言:assertTimeoutPreemptively

当我们希望测试耗时方法的执行时间,并不想让测试方法无限地等待时,就可以对测试方法进行超时测试,JUnit 5 对此推出了断言方法 assertTimeout,提供了对超时的广泛支持。

假设我们希望测试代码在一秒内执行完毕,可以写如下测试用例:

@Test
@DisplayName("超时方法测试")
void test_should_complete_in_one_second() {
  Assertions.assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000));
}

异常断言:assertThrows

针对带有异常抛出的代码,JUnit 5 提供了 Assertions#assertThrows(Class<T>, Executable) 来进行测试,第一个参数为异常类型,第二个为函数式接口参数,跟 Runnable 接口相似,不需要参数,也没有返回,并且支持 Lambda 表达式方式使用,具体使用方式可参考下方代码:

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


@DisplayName("测试类")
public class MyTest {
    @DisplayName("测试捕获的异常")
    @Test
    void assertThrowsException() {
        String str = null;
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            Integer.valueOf(str);
        });
    }
}

Maven 对 Junit5 的支持

2018 年 10 月 24 日 Maven 3.6.0 发布,Maven 才正式原生支持 Junit5。在这个版本中,Maven 团队一并发布了 Maven Surefire Plugin 2.22.0Maven Failsafe plugin 2.22.0,进而解决了对 Junit5 的支持问题。

在此之前,为了能在 Maven 中运行 Junit5 的测试用例,需要为 Maven Surefire plugin 额外提供一个 Junit5 团队提供的 Junit Provider

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <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.0</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>
</plugin>

Maven 升级到 3.6.0 及以上版本,junit-platform-surefire-provider 这个依赖就不需要了