如何在 Java 中开始单元测试:JUnit 5 完全入门
大家好,Dev.to 的朋友们!在这篇文章中,我想重点介绍使用 JUnit5 库进行 Java 单元测试。与之前的 JUnit 4 版本相比,JUnit5 引入了一些值得关注的新特性和测试方法。我们将概述什么是单元测试以及为什么要进行单元测试;如何在项目中安装 JUnit 5;什么是基本的测试结构;如何使用断言 API 以及如何将多个测试组合成一个测试套件。
顺便说一下,原文发布在我的博客上,你可以在这里找到它。
什么是单元测试?
单元测试是软件测试的一个级别,它将软件的各个组件隔离出来进行测试。例如,我们有一个UserService 组件。它可能关联着各种依赖项,例如用于连接数据源的UserDAO 组件,或者用于发送确认邮件的EmailProvider 组件。但在单元测试中,我们将UserService 组件隔离出来,并可能模拟其关联的依赖项(如何进行模拟,我们将在下一章中讨论)。
单元测试给我们带来了诸多好处,仅举几例:
- 它能增强我们修改代码时的信心。如果单元测试编写良好,并且每次代码更改后都运行它们,那么在引入新功能时,我们就能发现任何错误。
- 它起到文档的作用。当然,代码文档化包含多种工具,单元测试就是其中之一——它向其他开发人员描述了代码的预期行为。
- 它能提高代码的可重用性,因为好的单元测试需要代码组件模块化。
这些只是单元测试众多优势中的一小部分。现在,我们已经定义了什么是单元测试以及为什么要使用它,接下来就可以开始学习 JUnit 5 了。
安装 JUnit5
构建工具支持
要获得对 JUnit5 的原生支持,您应该拥有 Gradle 4.6+ 或 Maven 2.22.0+ 版本。
对于 Maven,您需要在pom.xml文件中添加:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>{$version}</version>
<scope>test</scope>
</dependency>
对于 Gradle,请添加到build.gradle 文件中:
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '$version'
您可以在官方仓库中找到最新版本的 JUnit5 。另外,如果您要将 JUnit 5 与 Vert.x 框架一起使用,可以使用Vert.x JUnit5 扩展。
IDE 支持
Intelij IDEA 自 2016.2 版本起原生支持 JUnit5,Eclipse 自 4.7.1a 版本起也支持 JUnit5。
单元测试剖析
基本结构
考虑以下示例:我们有一个程序,用于在整数数组中执行线性搜索。以下是主类,我们将其放在src/main/java/文件夹中:
class LinearSearcher(){
private int[] data;
LinearSearcher(int[] arr){
this.data = arr;
}
int getPositionOfNumber(int value){
int n = data.length;
for(int p = 0; i < n; i++)
{
if(data[p] == value)
return p;
}
return -1;
}
}
然后将以下第二段代码添加到src/test/java文件夹中:
class LinearSearcherTest{
private static LinearSearcher searcher;
//before
@BeforeAll
static void setup(){
int[] array = {2, 3, 4, 10, 40};
searcher = new LinearSearcher(array);
}
//Actual test methods
@Test
void getPosition(){
int result = searcher.getPositionOfNumber(10);
Assertions.assertEquals(3,result);
}
@Test
void noSuchNumber(){
int result = searcher.getPositionOfNumber(55);
Assertions.assertEquals(-1, result);
}
//after
@AfterAll
static void finish(){
System.out.println("Tests are finished!");
}
}
让我们来看看这段代码做了什么。我们引入了一个新的类LinearSearcher ,它有一个方法getPostionOfNumber,该方法返回数组中值的位置,如果数组中不存在该值,则返回 -1。
在第二类LinearSearcherTest中,我们实际进行单元测试。我们预期会出现两种情况:当数组中存在一个数字(在本例中为 10)时,我们期望返回该数字的位置(例如 3)。如果数组中不存在该数字(例如 55),则搜索器应返回 -1。现在,您可以运行这段代码并检查结果。
方法之前
你会注意到有两个方法分别用@BeforeAll和@AfterAll注解。它们的作用是什么?第一个方法对应于“之前”方法。这类方法有两个:
- @BeforeAll -静态方法,将在当前类中所有 @test方法执行之前执行一次。
- @BeforeEach - 将在当前类中每个 @test方法之前执行的方法。
这些方法对于设置单元测试环境(例如,创建实例)非常方便。
方法之后
就像有前置方法一样,也有后置方法。而且,后置方法还有以下几种:
使用标准断言 API
断言 API是一系列实用方法的集合,用于支持在测试中断言条件。虽然有很多可用方法,但我们将重点介绍其中最重要的几种。
断言非空
当我们需要断言实际对象不为空时,可以使用以下方法:
assertNotNull(Object obj);
如果对象不为空,则方法通过;否则,方法失败。
断言等于
这个组包含很多方法,所以我不会提供所有重载版本,而是重点介绍一个通用签名:
assertEquals(expected_value, actual_value, optional_message);
这些方法有两个必需参数和一个可选参数:
- expected_value = 我们想要接收的结果
- 实际值 = 测试值
- optional_message = String message,如果方法失败,则会显示在 STDOUT 上。
值可以是基本类型:int、double、float、long、short、boolean、char、byte,以及字符串和对象。我们可以将以下测试方法添加到此组中:
- assertArrayEquals - 检查预期数组和实际数组是否相等。数组均为基本类型。
- AssertFalse和AssertTrue分别检查提供的布尔条件是否为假或为真。
- assertIterableEquals - 与 assertArrayEquals 相同,但适用于可迭代对象(例如 List、Set 等)
正如我提到的,本节中有很多重载方法,因此值得查阅官方文档以获取具体的签名。
断言抛出
这是 JUnit5 的一项创新。假设你有一个会抛出异常的方法:
Car findCarById(String id) throws FailedProviderException;
此方法通过 ID 从底层数据库中检索单个车辆,并在数据库出现问题时抛出 FailedProviderException 异常。换句话说,我们将可能出现的数据源异常(例如 SQLException 或 NoSQL 数据库的异常)封装在一个接口中,从而实现了与具体实现的独立性。
我们如何测试是否抛出了异常?以前在 JUnit4 中,我们使用注解:
@Test(expected = FailedProviderException.class)
void exceptionThrownTest() throws Exception{
Car result = repository.findCarById("non-existing-id");
}
顺便一提,TestNG 也使用了相同的思路。JUnit5 引入了assertThrows方法。请看,我们会如何处理这种情况:
@Test
void exceptionThrownTest(){
Assertions.assertThrows(FailedProviderException.class, ()->{
Car result = repository.findCarById("non-existing-id");
});
}
此方法签名包含两个部分:
- 预期会抛出异常
- 包含代码片段的可执行文件的 Lambda 表达式,可能会抛出异常。
同样,正如我们前面提到的 assertEquals 方法组的方法一样,我们可以提供一个可选的字符串消息作为第三个参数。
断言超时
当我们需要断言测试在设定的超时时间内完成时,可以使用以下方法:
assertTimeout(Duration timeout, Executable executable)
其思路与 assertThrows 方法相同,但这里我们需要指定超时时间。第二个参数是一个相同的可执行 lambda 表达式。第三个可选参数是一个字符串消息。让我们来看一个例子:
@Test
void in3secondsTest(){
Assertions.assertTimeout(Duration.ofSeconds(3), ()->{
//some code
});
}
请注意,此方法使用Duration API来指定时间范围。它提供了一些便捷的方法,例如 ofSeconds()、ofMills() 等。如果您还不熟悉 Duration API,不妨查看一下这篇教程。
失败
最后,如果我们需要让测试失败怎么办?只需使用Assertions.fail()方法即可。同样,有好几种方法:
- fail(字符串消息)= 使用给定的失败消息执行测试失败。
- fail (String message, Throwable cause) = 使用给定的失败消息以及根本原因使测试失败。
- 失败(可抛出原因)= 因给定的根本原因导致测试失败。
创建测试套件
如果您有多个单元测试,并且想要一次性执行它们,您可以创建一个测试套件。
这种方法允许你将测试分散到多个测试类和不同的包中运行。
假设我们有测试用例 TestA、TestB 和 TestC,它们分别位于三个包中:net.mednikov.teststutorial.groupA、net.mednikov.teststutorial.groupA 和 net.mednikov.teststutorial.groupC。我们可以编写测试套件来将它们组合起来:
@RunWith(JUnitPlatform.class)
@SelectPackages({net.mednikov.teststutorial.groupA, net.mednikov.teststutorial.groupB, net.mednikov.teststutorial.groupC})
public class TestSuite(){}
现在,您可以将此方法作为一个测试套件来运行。
参考
- Sergio Martin,《使用 JUnit 5 将单元测试提升到新水平》(2018)。DZone,点击此处阅读。
- Petri Kainulainen,《使用 JUnit5 断言 API 编写断言》(2018),点击此处阅读。
- J Steven Perry,《JUnit5 Jupiter API》(2017),IBM Developer,点击此处阅读。
结论
在这篇文章中,我们学习了什么是单元测试以及为什么要进行单元测试;如何在项目中安装 JUnit 5;什么是基本的测试结构;如何使用断言 API 以及如何将来自不同包的多个测试组合到一个测试套件中。当然,JUnit 5 是一个非常庞大的主题,这篇文章只是冰山一角。一些框架,例如 Vert.x,提供了专门的 JUnit 5 扩展,例如vertx-junit5。祝您使用 JUnit 5 一切顺利!:)
文章来源:https://dev.to/iuriimednikov/how-to-start-with-unit-testing-in-java-a-complete-introduction-to-junit-5-3cc