单元测试是软件开发中必不可少的一环,可是在日常开发中每每由于项目周期紧,工做量大而被选择忽略,这样每每致使软件问题层出不穷。线上出现的很多问题其实在有单元测试的状况下就能够及时发现和处理,所以培养本身在平常开发中写单元测试的能力是颇有必要的。不管是对本身的编码能力的提升,仍是项目质量的提高,都是大有好处,本文将介绍 Java 单元测试框架 JUnit 5 的基础认识和使用来编写单元测试,但愿一样对你有所帮助。java
本文所涉及全部代码片断均在下面仓库中,感兴趣的小伙伴欢迎参考学习:git
https://github.com/wrcj12138a...github
版本支持:编程
- JDK 8
- JUnit 5.5.2
- Lomok 1.18.8
要说什么是 JUnit 5,首先就得聊下 Java 单元测试框架 JUnit,它与另外一个框架 TestNG 占据了 Java领域里单元测试框架的主要市场,其中 JUnit 有着较长的发展历史和不断演进的丰富功能,备受大多数 Java 开发者的青睐。segmentfault
而说到 JUnit 的历史,JUnit 起源于 1997年,最第一版本是由两位编程大师 Kent Beck 和 Erich Gamma 的一次飞机之旅上完成的,因为当时 Java 测试过程当中缺少成熟的工具,两人在飞机上就合做设计实现了 JUnit 雏形,旨在成为更好用的 Java 测试框架。现在二十多年过去了,JUnit 通过各个版本迭代演进,已经发展到了 5.x 版本,为 JDK 8以及更高的版本上提供更好的支持 (如支持 Lambda ) 和更丰富的测试形式 (如重复测试,参数化测试)。api
了解过 JUint 以后,再回头来看下 JUnit 5,这个版本能够说是 JUnit 单元测试框架的一次重大升级,首先须要 Java 8 以上的运行环境,虽然在旧版本 JDK 也能编译运行,但要彻底使用 JUnit 5 功能, JDK 8 环境是必不可少的。数组
除此以外,JUnit 5 与之前版本的 JUnit 不一样,拆分红由三个不一样子项目的几个不一样模块组成。架构
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
基于上面的介绍,能够参考下图对 JUnit 5 的架构和模块有所了解:框架
说完 JUnit 5 是什么以后,咱们再来想一个问题:为何须要一个 JUnit 5 呢?ide
自从有了相似 JUnit 之类的测试框架,Java 单元测试领域逐渐成熟,开发人员对单元测试框架也有了更高的要求:更多的测试方式,更少的其余库的依赖。所以,你们期待着一个更强大的测试框架诞生,JUnit 做为Java测试领域的领头羊,推出了 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 注解,代表该测试方法处于不可用,执行测试类的测试方法时不会被 JUnit 执行。
下面看下使用 @Disbaled 以后的运行效果,在原来测试类中添加以下代码:
@DisplayName("个人第三个测试") @Disabled @Test void testThirdTest() { System.out.println("个人第三个测试开始测试"); }
运行后看到控制台日志以下,用 @Disabled 标记的方法不会执行,只有单独的方法信息打印:
@Disabled 也可使用在类上,用于标记类下全部的测试方法不被执行,通常使用对多个测试类组合测试的时候。
当咱们编写的类和代码逐渐增多,随之而来的须要测试的对应测试类也会愈来愈多。为了解决测试类数量爆炸的问题,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("第二个内嵌测试类执行测试"); } } }
运行全部测试用例后,在控制台能看到以下结果:
在 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 错误进行抛出提示。
当咱们但愿测试耗时方法的执行时间,并不想让测试方法无限地等待时,就能够对测试方法进行超时测试,JUnit 5 对此推出了断言方法 assertTimeout
,提供了对超时的普遍支持。
假设咱们但愿测试代码在一秒内执行完毕,能够写以下测试用例:
@Test @DisplayName("超时方法测试") void test_should_complete_in_one_second() { Assertions.assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000)); }
这个测试运行失败,由于代码执行将休眠两秒钟,而咱们指望测试用例在一秒钟以内成功。可是若是咱们把休眠时间设置一秒钟,测试仍然会出现偶尔失败的状况,这是由于测试方法执行过程当中除了目标代码还有额外的代码和指令执行会耗时,因此在超时限制上没法作到对时间参数的彻底精确匹配。
咱们代码中对于带有异常的方法一般都是使用 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-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 是 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 里每一个参数都会运行目标方法,一旦哪一个参数运行测试失败,就意味着该测试方法不经过。
经过 @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 还提供了如下三种数据源:
provideArguments
方法能够返回自定义类型的 Stream<Arguments> ,做为测试方法所须要的数据使用。对上面三种数据源注解感兴趣的同窗能够参考示例工程的 ParameterizedUnitTest 类,这里就不一一再介绍了。
到这里,想必你对 JUnit 5 也有了基本的了解和掌握,都说单元测试是提高软件质量,提高研发效率的必备环节,从会用 JUnit 5 写单元测试开始,培养写测试代码的习惯,在不断实践中提高自身的开发效率,让写出来的代码有更质量的保证。