点击蓝色“程序猿DD”关注我

回复“资源”获取独家整理的学习资料!

作者 | 闻人

来源 | 公众号「闻人的技术博客」

前言

单元测试是软件开发中必不可少的一环,但是在平常开发中往往因为项目周期紧,工作量大而被选择忽略,这样往往导致软件问题层出不穷。线上出现的不少问题其实在有单元测试的情况下就可以及时发现和处理,因此培养自己在日常开发中写单元测试的能力是很有必要的。无论是对自己的编码能力的提高,还是项目质量的提升,都是大有好处,本文将介绍 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 {@BeforeAllpublic static void init() {System.out.println("初始化数据");}@AfterAllpublic static void cleanup() {System.out.println("清理数据");}@BeforeEachpublic void tearup() {System.out.println("当前测试方法开始");}@AfterEachpublic void tearDown() {System.out.println("当前测试方法结束");}@DisplayName("我的第一个测试")@Testvoid testFirstTest() {System.out.println("我的第一个测试开始测试");}@DisplayName("我的第二个测试")@Testvoid 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 {@BeforeEachvoid init() {System.out.println("测试方法执行前准备");}@Nested@DisplayName("第一个内嵌测试类")class FirstNestTest {@Testvoid test() {System.out.println("第一个内嵌测试类执行测试");}}@Nested@DisplayName("第二个内嵌测试类")class SecondNestTest {@Testvoid 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\

    ,作为测试方法所需要的数据使用。

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

结语

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

-END-

留言交流不过瘾

关注我,回复“加群”加入各种主题讨论群

点一点“阅读原文”小惊喜在等你

Java单元测试之JUnit 5快速上手相关推荐

  1. Java单元测试之模拟利器-使用PowerMock进行Mock测试

    首页 国产Linux Linux命令 openSUSE ArchLinux Slackware FreeBSD Ubuntu CentOS Fedora Debian PHP教程 在线教程 登录 注册 ...

  2. Java单元测试之JUnit4详解

    2019独角兽企业重金招聘Python工程师标准>>> Java单元测试之JUnit4详解 与JUnit3不同,JUnit4通过注解的方式来识别测试方法.目前支持的主要注解有: @B ...

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

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

  4. java单元测试之mock篇

    java单元测试之mock篇 一.什么是mock? 二.为什么要进行mock? 三.IDEA中使用Mock 3.1.引入mock所需依赖 3.1.IDEA单元测试必备快捷键 3.2.Mock测试相关注 ...

  5. 单元测试之—JUnit与SpringTest的完美结合

    原文作者:Veiking 原文地址:Spring回顾之五 -- 测试,JUnit与SpringTest的完美结合 目录 第一步:JUnit与SpringTest的引入 第二步:JUnit测试验证 第三 ...

  6. 单元测试之JUnit 5 参数化测试使用手册

    1. 概要 junit5是下一代JUnit测试框架,新增了很多特性帮助开发人员更好得编写测试用例.其中一大特性就是参数化测试,其目的就是让我们可以使用不同的参数多次执行一个测试方法,从而覆盖不同的条件 ...

  7. 软件测试之单元测试之Junit测试使用前准备

    Junit测试只能说是算的上软件测试这门课中首次出现需要实操进行测试的工具了. 可以理解为前面的黑盒测试,白盒测试是为了理解一种测试思想,根据测试思想能够进行设计测试样例,到了Junit单元测试这里, ...

  8. clojure java math_Clojure学习笔记 02.快速上手

    这一节让我们来了解一下那些"使用Clojure编程必须要掌握的东西". 其实Clojure(以及其他lisp系语言)的学习曲线还是比较陡峭的.学习Clojure之前必须要掌握一些基 ...

  9. Java该如何自学才能快速上手?

    如何快速入门Java? 一.作为刚接触Java的新手,肯定要先了解Java的基础.         Java 基本语法.Java 平台应用.Java的核心概念:JVM.JDK.JRE以及Java面向对 ...

  10. Java单元测试之Mock框架

    一.引言 二.为什么要用Mock 三.Mock使用场景 四.Mock定义 五.Mock框架 五.Mockito 5.1 Mockito基本使用 5.2 MockMVC测试 5.2.1 初始化MockM ...

最新文章

  1. kotlin ++ --_顺便说一句-探索Kotlin代表团
  2. java list用法_java list的用法详解
  3. php cros跨域处理,php接口设置cors实现ajax跨域
  4. go mysql教学_Go语言之对Mysql简单操作
  5. scala学习笔记-数组转换(8)
  6. SAP Spartacus的登录页面的用户名显示逻辑
  7. 用python生成云词汇_用python生成词云wordcloud
  8. @PostConstruct注解详解
  9. Django——序列化与反序列化
  10. 使用nvDXT.exe把图片转换成dds图片【转】
  11. DRM在Android中
  12. 正点原子STM32(基于HAL库)1
  13. Kaggle:Video Game Sales电子游戏销售分析(Tableau展示)
  14. hdu 1824 Let's go home(2-sat 基础题)
  15. python温度转换_python第一课:温度转换
  16. libusb Zadig pyusb
  17. java硬币兑换_java动态规划取硬币问题
  18. R语言xlsx包读取含中文Excel文件时出现乱码或“多字节字符串6有错”的解决方案
  19. 小白必读-所有测试大咖都知道的自动化知识
  20. xpraid安装_如何把SATA/RAID驱动集成到win xp 安装盘里

热门文章

  1. DART booster
  2. 电脑开机显示器黑屏是什么原因,电脑显示器黑屏怎么办
  3. 双线双IP和BGP双线有什么区别?
  4. Excel快速拆分单元格内容
  5. IMO模型编程思维法(Input-Model-Output)
  6. Dojo1.11官方教程文档翻译(3.4)Dojo特效
  7. [渝粤教育] 中国地质大学 信息资源管理 复习题
  8. Dapper基础入门
  9. 穿山甲android对接错误码40029,头条 穿山甲广告 错误码列表
  10. 基于ArduPilot Rover 4.1.5版本的汉特四驱小车搭建(1)