什么是 JUnit
JUnit 是 Java 的单元测试框架, 起源于 1997 年,最初版本是由两位编程大师 Kent Beck 和 Erich Gamma 的一次飞机之旅上完成的,由于当时 Java 测试过程中缺乏成熟的工具,两人在飞机上就合作设计实现了 JUnit 雏形, 经过多个版本迭代演进,现已成为 java 单元测试的事实标准
JUnit 隶属于 xUnit, xUnit 是一套基于测试驱动开发的测试框架,包括:
- 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 5 对 JUnit 4 中的注解进行了变更和补充 ,以下是对照表
JUnit 4 | JUnit 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 5 的 api 均来自 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 5 在 JUnit 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.0 和 Maven 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
这个依赖就不需要了