前言

单元测试是软件开发中必不可少的一环,但是在平常开发中往往因为项目周期紧,工作量大而被选择忽略,这样往往导致软件问题层出不穷。线上出现的不少问题其实在有单元测试的情况下就可以及时发现和处理,因此培养自己在日常开发中写单元测试的能力是很有必要的。无论是对自己的编码能力的提高,还是项目质量的提升,都是大有好处,本文将介绍 Java 单元测试框架 JUnit 5 的基础认识和使用来编写单元测试,希望同样对你有所帮助。

本文所涉及所有代码片段均在下面仓库中,感兴趣的小伙伴欢迎参考学习:

https://github.com/wrcj12138aaa/junit5-actions

版本支持:

  • JDK 8
  • JUnit 5.5.2
  • Lomok 1.18.8

认识 JUnit 5

要说什么是 JUnit 5,首先就得聊下 Java 单元测试框架 JUnit,它与另一个框架 TestNG 占据了 Java领域里单元测试框架的主要市场,其中 JUnit 有着较长的发展历史和不断演进的丰富功能,备受大多数 Java 开发者的青睐。

而说到 JUnit 的历史,JUnit 起源于 1997年,最初版本是由两位编程大师 Kent Beck 和 Erich Gamma 的一次飞机之旅上完成的,由于当时 Java 测试过程中缺乏成熟的工具,两人在飞机上就合作设计实现了 JUnit 雏形,旨在成为更好用的 Java 测试框架。如今二十多年过去了,JUnit 经过各个版本迭代演进,已经发展到了 5.x 版本,为 JDK 8以及更高的版本上提供更好的支持 (如支持 Lambda ) 和更丰富的测试形式 (如重复测试,参数化测试)。

了解过 JUint 之后,再回头来看下 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 Jupiter:包含 JUnit 5 新的编程模型和扩展模型,主要就是用于编写测试代码和扩展代码。

  • JUnit Vintage:用于在JUnit 5 中兼容运行 JUnit3.x 和 JUnit4.x 的测试用例。

基于上面的介绍,可以参考下图对 JUnit 5 的架构和模块有所了解:

为什么需要 JUnit 5

说完 JUnit 5 是什么之后,我们再来想一个问题:为什么需要一个 JUnit 5 呢?

自从有了类似 JUnit 之类的测试框架,Java 单元测试领域逐渐成熟,开发人员对单元测试框架也有了更高的要求:更多的测试方式,更少的其他库的依赖。因此,大家期待着一个更强大的测试框架诞生,JUnit 作为Java测试领域的领头羊,推出了 JUnit 5 这个版本,主要特性:

  • 提供全新的断言和测试注解,支持测试类内嵌

  • 更丰富的测试方式:支持动态测试,重复测试,参数化测试等

  • 实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖

  • 提供对 Java 8 的支持,如 Lambda 表达式,Sream API等。

JUnit 5 常见用法介绍

接下来,我们看下 JUni 5 的一些常见用法,来帮助我们快速掌握 JUnit 5 的使用。

首先,在 Maven 工程里引入 JUnit 5 的依赖坐标,需注意的是当前JDK 环境要在 Java 8 以上。

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

第一个测试用例

引入JUnit 5,我们可以先快速编写一个简单的测试用例,从这个测试用例来认识初步下 JUnit 5:

@DisplayName("我的第一个测试用例")
public class MyFirstTestCaseTest { @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("我的第一个测试")
@Test
void testFirstTest() {
System.out.println("我的第一个测试开始测试");
} @DisplayName("我的第二个测试")
@Test
void testSecondTest() {
System.out.println("我的第二个测试开始测试");
}
}

直接运行这个测试用例,可以看到控制台日志如下:

可以看到左边一栏的结果里显示测试项名称就是我们在测试类和方法上使用 @DisplayName 设置的名称,这个注解就是 JUnit 5 引入,用来定义一个测试类并指定用例在测试报告中的展示名称,这个注解可以使用在类上和方法上,在类上使用它就表示该类为测试类,在方法上使用则表示该方法为测试方法。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = STABLE, since = "5.0")
public @interface DisplayName {
String value();
}

再来看下示例代码中使用到的一对注解 **@BeforeAll **和 @AfterAll ,它们定义了整个测试类在开始前以及结束时的操作,只能修饰静态方法,主要用于在测试过程中所需要的全局数据和外部资源的初始化和清理。与它们不同,@BeforeEach@AfterEach 所标注的方法会在每个测试用例方法开始前和结束时执行,主要是负责该测试用例所需要的运行环境的准备和销毁。

在测试过程中除了这些基本的注解,还有更多丰富强大的注解,接下来就我们一一学习下吧。

禁用执行测试:@Disabled

当我们希望在运行测试类时,跳过某个测试方法,正常运行其他测试用例时,我们就可以用上 @Disabled 注解,表明该测试方法处于不可用,执行测试类的测试方法时不会被 JUnit 执行。

下面看下使用 @Disbaled 之后的运行效果,在原来测试类中添加如下代码:

@DisplayName("我的第三个测试")
@Disabled
@Test
void testThirdTest() {
System.out.println("我的第三个测试开始测试");
}

运行后看到控制台日志如下,用 @Disabled 标记的方法不会执行,只有单独的方法信息打印:

@Disabled 也可以使用在类上,用于标记类下所有的测试方法不被执行,一般使用对多个测试类组合测试的时候。

内嵌测试类:@Nested

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

@DisplayName("内嵌测试类")
public class NestUnitTest {
@BeforeEach
void init() {
System.out.println("测试方法执行前准备");
} @Nested
@DisplayName("第一个内嵌测试类")
class FirstNestTest {
@Test
void test() {
System.out.println("第一个内嵌测试类执行测试");
}
} @Nested
@DisplayName("第二个内嵌测试类")
class SecondNestTest {
@Test
void test() {
System.out.println("第二个内嵌测试类执行测试");
}
}
}

运行所有测试用例后,在控制台能看到如下结果:

重复性测试:@RepeatedTest

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

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

运行后测试方法会执行3次,在 IDEA 的运行效果如下图所示:

这是基本的用法,我们还可以对重复运行的测试方法名称进行修改,利用 @RepeatedTest 提供的内置变量,以占位符方式在其 name 属性上使用,下面先看下使用方式和效果:

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

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

新的断言

在断言 API 设计上,JUnit 5 进行显著地改进,并且充分利用 Java 8 的新特性,特别是 Lambda 表达式,最终提供了新的断言类: org.junit.jupiter.api.Assertions 。许多断言方法接受 Lambda 表达式参数,在断言消息使用 Lambda 表达式的一个优点就是它是延迟计算的,如果消息构造开销很大,这样做一定程度上可以节省时间和资源。

现在还可以将一个方法内的多个断言进行分组,使用 assertAll 方法如下示例代码:

@Test
void testGroupAssertions() {
int[] numbers = {0, 1, 2, 3, 4};
Assertions.assertAll("numbers",
() -> Assertions.assertEquals(numbers[1], 1),
() -> Assertions.assertEquals(numbers[3], 3),
() -> Assertions.assertEquals(numbers[4], 4)
);
}

如果分组断言中任一个断言的失败,都会将以 MultipleFailuresError 错误进行抛出提示。

超时操作的测试:assertTimeoutPreemptively

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

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

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

这个测试运行失败,因为代码执行将休眠两秒钟,而我们期望测试用例在一秒钟之内成功。但是如果我们把休眠时间设置一秒钟,测试仍然会出现偶尔失败的情况,这是因为测试方法执行过程中除了目标代码还有额外的代码和指令执行会耗时,所以在超时限制上无法做到对时间参数的完全精确匹配。

异常测试:assertThrows

我们代码中对于带有异常的方法通常都是使用 try-catch 方式捕获处理,针对测试这样带有异常抛出的代码,而 JUnit 5 提供方法 Assertions#assertThrows(Class<T>, Executable) 来进行测试,第一个参数为异常类型,第二个为函数式接口参数,跟 Runnable 接口相似,不需要参数,也没有返回,并且支持 Lambda表达式方式使用,具体使用方式可参考下方代码:

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

当Lambda表达式中代码出现的异常会跟首个参数的异常类型进行比较,如果不属于同一类异常,就会控制台输出如下类似的提示:org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <IllegalArgumentException> but was: <...Exception>

JUnit 5 参数化测试

要使用 JUnit 5 进行参数化测试,除了 junit-jupiter-engine 基础依赖之外,还需要另个模块依赖:junit-jupiter-params,其主要就是提供了编写参数化测试 API。同样方式,把相同版本的对应依赖引入 Maven 工程中:

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

基本数据源测试: @ValueSource

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

public class ParameterizedUnitTest {
@ParameterizedTest
@ValueSource(ints = {2, 4, 8})
void testNumberShouldBeEven(int num) {
Assertions.assertEquals(0, num % 2);
} @ParameterizedTest
@ValueSource(strings = {"Effective Java", "Code Complete", "Clean Code"})
void testPrintTitle(String title) {
System.out.println(title);
}
}

@ParameterizedTest 作为参数化测试的必要注解,替代了 @Test 注解。任何一个参数化测试方法都需要标记上该注解。

运行测试,结果如下图所示,针对 @ValueSource 里每个参数都会运行目标方法,一旦哪个参数运行测试失败,就意味着该测试方法不通过。

CSV 数据源测试:@CsvSource

通过 @CsvSource 可以注入指定 CSV 格式 (comma-separated-values) 的一组数据,用每个逗号分隔的值来匹配一个测试方法对应的参数,下面是使用示例:

@ParameterizedTest
@CsvSource({"1,One", "2,Two", "3,Three"})
void testDataFromCsv(long id, String name) {
System.out.printf("id: %d, name: %s", id, name);
}

运行结果如图所示,除了用逗号分隔参数外,@CsvSource 还支持自定义符号,只要修改它的 delimiter 即可,默认为

JUnit 还提供了读取外部 CSV 格式文件数据的方式作为数据源的实现,我们只要用 @CsvFileSource 指定资源文件路径即可,使用起来跟 @CsvSource 一样简单这里就不再重复演示了。

@CsvFileSource 指定的资源文件路径时要以 / 开始,寻找当前测试资源目录下文件。

除了上面提到的三种数据源方式外,JUnit 还提供了以下三种数据源:

  • @EnumSource:允许我们通过参数值,给指定 Enum 枚举类型传入,构造出枚举类型中特定的值。
  • @MethodSource:指定一个返回的 Stream / Array / 可迭代对象 的方法作为数据源。 需要注意的是该方法必须是静态的,并且不能接受任何参数。
  • @ArgumentSource:通过实现 ArgumentsProvider 接口的参数类来作为数据源,重写它的 provideArguments 方法可以返回自定义类型的 Stream<Arguments> ,作为测试方法所需要的数据使用。

对上面三种数据源注解感兴趣的同学可以参考示例工程的 ParameterizedUnitTest 类,这里就不一一再介绍了。

结语

到这里,想必你对 JUnit 5 也有了基本的了解和掌握,都说单元测试是提升软件质量,提升研发效率的必备环节,从会用 JUnit 5 写单元测试开始,培养写测试代码的习惯,在不断实践中提升自身的开发效率,让写出来的代码有更质量的保证。

推荐阅读

参考资料

Java单元测试之JUnit 5快速上手的更多相关文章

  1. Java单元测试之JUnit篇

    单元测试是编写测试代码,应该准确.快速地保证程序基本模块的正确性. 好的单元测试的标准 JUnit是Java单元测试框架,已经在Eclipse中默认安装. JUnit4 JUnit4通过注解的方式来识 ...

  2. Java基础学习总结(24)——Java单元测试之JUnit4详解

    Java单元测试之JUnit4详解 与JUnit3不同,JUnit4通过注解的方式来识别测试方法.目前支持的主要注解有: @BeforeClass 全局只会执行一次,而且是第一个运行 @Before  ...

  3. 简明易懂,将细节隐藏,面向新手树立web开发概念——学完Java基础语法,超快速上手springboot+mybatiJavaWeb开发

    简明易懂,将细节隐藏,面向新手树立web开发概念 --学完Java基础语法,超快速上手JavaWeb开发 Web本质(先忽视各种协议) Web应用可以理解为浏览器和服务器之间的交互. 我们可以看一个简 ...

  4. java单元测试之如何实现异步接口的测试案例

    测试是软件发布的重要环节,单元测试在实际开发中是一种常用的测试方法,java单元测试主要用junit,最新是junit5,本人开发一般用junit4.因为单元测试能够在软件模块组合之前尽快发现问题,所 ...

  5. Java单元测试之覆盖率统计eclemma

    安装 有两种安装方法 下载安装(推荐) 地址: http://sourceforge.net/projects/eclemma/ 将解压后的features和plugins目录下的文件分别拷贝到Ecl ...

  6. java单元测试之Mock静态方法

    1 public final class AmountUtil { public static String CustomFormatWith2Digits(int amount) { return ...

  7. Spring Boot 揭秘与实战(一) 快速上手

    文章目录 1. 简介 1.1. 什么是Spring Boot 1.2. 为什么选择Spring Boot 2. 相关知识 2.1. Spring Boot的spring-boot-starter 2. ...

  8. Java 程序员快速上手 Kotlin 11 招

    欢迎大家关注腾讯云技术社区-博客园官方主页,我们将持续在博客园为大家推荐技术精品文章哦~ 作者:霍丙乾 近经常会收到一些 "用 Kotlin 怎么写" 的问题,作为有经验的程序员, ...

  9. Java开发快速上手

    Java开发快速上手 前言 1.我的大学 2.对初学者的建议 3.大牛的三大特点 4.与他人的差距 第一章 了解Java开发语言 前言 基础常识 1.1 什么是Java 1.1.1 跨平台性 1.2 ...

随机推荐

  1. golang 结合实例更好的理解参数传递和指针

    关于参数传递 其实go的参数传递,核心就是一句话:go里所有参数传递都是值传递,既把参数复制一份放到函数里去用. go的函数传参,不管参数是什么类型,都会复制一份,然后新的参数在函数内部被使用. 不像 ...

  2. windiows下搭建python+selenium+unittest+Chrome的Web自动化环境

    一.selenium.unittest概念 Selenium 是用于测试 Web 应用程序用户界面 (UI) 的常用框架.它是一款用于运行端到端功能测试的超强工具.您可以使用多个编程语言编写测试,并且 ...

  3. lnmp环境搭建方法

    网上目前的一键搭建方法: 命令行安装: 1.源码编译安装:(个性化配置,安装配置过程繁琐) 2.使用yum或apt直接安装:(使用编译好的二进制文件安装,速度快) 3.军哥的lnmp一键脚本安装: 4 ...

  4. 谈自由,ASP.NET Core才是未来?

    首先我要说一下自己对自由的理解: 自由是我可以选择不干什么,但我要保留我可以干什么的可能性. 比如说我现在只有一个码农的角色,但我仍然要保留我可以扮演其他角色的可能, 比如成为一个作者,当我写下文章的 ...

  5. 重新学习MySQL数据库开篇:数据库的前世今生

    本文内容出自刘欣的"码农翻身"公众号,强烈推荐刘欣大大的文章.   数据库的前世今生 小李的数据库之旅 无纸化办公 小李是这个大学计算机科学与技术系的知名学生,他的编程能力了得,使 ...

  6. 0807 创建vue实例以及vue的基础指令

    lession1 1.Vue的了解   渐进式框架   作者:尤雨溪     mvvm 2.创建vue实例 引入<script src="vue.js"><scr ...

  7. 玩转 SpringBoot 2 快速搭建 | Spring Tool Suite篇

    Spring Tool Suite (STS) 工具介绍 我个人比较推荐使用 Spring Tool Suite(STS),之所以推荐使用 Spring Tool Suite(STS) ,是因为它是 ...

  8. Linux下各目录及其作用

    目录及其作用 /:根目录,一般根目录下只存放目录,不要存放件,/etc./bin./dev./lib./sbin应该和根目录放置在一个分区中 /bin: /usr/bin: 可执行二进制文件的目录,如 ...

  9. 从零开始搭建Java开发环境第二篇:如何在windows10里安装MySQL

    1 下载安装包 1.1 压缩包 https://dev.mysql.com/downloads/mysql/ [外链图片转存失败(img-oesO8K09-1566652568838)(data:im ...

  10. 微信支付之扫码、APP、小程序支付接入详解

    做电商平台的小伙伴都知道,支付服务是必不可少的一部分,今天我们开始就说说支付服务的接入及实现.目前在国内,几乎90%中小公司的支付系统都离不开微信支付和支付宝支付.那么大家要思考了,为什么微信支付和支 ...