单元测试
目录:
文章目录
Test-driven development
简介
测试驱动开发(TDD)是一个将编码、测试和设计交织在一起的软件开发过程。它是一种测试优先的方法,旨在提高应用程序的质量。测试驱动开发由以下生命周期
定义
添加一个测试。
运行所有测试并观察新测试是否失败。
实现的代码。
运行所有测试并观察新测试是否成功。
重构代码。
JUnit 5
一、简介
-
JUnit 5包在org.junit.jupiter;
-
JUnit 5最低需要JDK8;
-
注解变动,相较于Junit4:
JUnit 4 JUnit 5 @Before @BeforeEach @BeforeClass @BeforeAll @After @AfterEach @AfterClass @AfterAll @Ignore @Disabled @Category @Tag -
JUnit 5 新加了一批断言方法;
-
Runners have been replaced with extensions, with a new API for extension implementors.
-
JUnit 5 introduces assumptions that stop a test from executing.
-
JUnit 5 supports nested and dynamic test classes.
二、Assertions类
方法 | 功能描述 |
---|---|
assertArrayEquals | 比较实际数组与期望数组内容 |
assertEquals | 比较实际值与期望值 |
assertNotEquals | 比较两个值,以验证它们不相等 |
assertTrue | 验证提供的值是true |
assertFalse | 验证提供的值是false |
assertLinesMatch | 比较两个字符串列表 |
assertNull | 验证提供的值是null |
assertNotNull | 验证提供的值不是null |
assertSame | 验证两个值引用同一个对象 |
assertNotSame | 验证两个值没有引用同一个对象 |
assertThrows | 验证方法的执行是否抛出预期的异常 |
assertTimeout | 验证所提供的函数是否在指定的超时时间内完成 |
assertTimeoutPreemptively | 验证所提供的函数是否在指定的超时时间内完成,但一旦达到超时,将终止函数的执行 |
注意:当在assertEquals中使用float和double值时,还可以指定一个delta,表示两者之间的差值阈值。在我们的示例中,我们可以添加0.001的增量,以防0.75实际上返回为0.750001。
分析测试结果
除了验证值或行为外,assert方法还可以接受错误的文本描述,这可以帮助您诊断失败。例如:
Assertions.assertEquals(0.75, result, "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4");
Assertions.assertEquals(0.75, result, () -> "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4");
这两种变体之间的区别在于,第一个变体总是创建消息,即使它没有显示,而第二个变体只在断言失败时构造消息。
还可以给测试方法添加@DisplayName注解,来更好的标识改测试:
@Test
@DisplayName("Test successful decimal conversion")
void testConvertToDecimalSuccess() {
double result = MathTools.convertToDecimal(3, 4);
Assertions.assertEquals(0.751, result);
}
三、运行单元测试
为了在Maven项目中运行JUunit 5测试,你需要在maven的pom.xml文件中添加maven-surefire-plugin和新的依赖,如下所示:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.javaworld.geekcap</groupId>
<artifactId>junit5</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
</plugin>
</plugins>
</build>
<name>junit5</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
JUnit 5 依赖
JUnit 5将其组件打包在org.junit.jupiter组中,我们需要添加JUnit -jupiter工件,这是一个聚合器工件,它导入以下依赖项:
- junit-jupiter-api,定义用于编写测试和扩展的API。
- junit-jupiter-engine, 是运行单元测试的测试引擎实现。
- junit-jupiter-params, 为参数化测试提供支持。
注意:确保在Java 8或更高版本中包含maven-编译器-插件,这样您就能够使用像lambda这样的Java 8特性。
运行!
mvn clean test
如果成功,会有类似输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MtXKwHOa-1627574379354)(https://i.loli.net/2021/07/29/bmfqSKHdWr4QltC.png)]
四、JUnit 5 中的参数化测试
以上是JUnit 5基础的参数化测试,接下来,会使用参数化测试来更彻底的测试代码
在MathTools类中添加如下方法:
public static boolean isEven(int number) {
return number % 2 == 0;
}
我们可以像之前一样,一个个输入值去测试:
@Test
void testIsEvenSuccessful() {
Assertions.assertTrue(MathTools.isEven(2));
Assertions.assertFalse(MathTools.isEven(1));
}
这样的方式奏效时奏效,但是当我们想要测试大量的值得时候,还采用这样的方式就显得太过笨重,因此我们可以使用参数化测试:
@ParameterizedTest
@ValueSource(ints = {0, 2, 4, 6, 8, 10, 100, 1000})
void testIsEven(int number) {
Assertions.assertTrue(MathTools.isEven(number));
}
在参数化测试中使用源
有多种不同类型的源,但是最简单的实现是@ValueSource,它允许我们指定一个数值列表或者字符串列表。它可以被当做参数传递给我们的测试方法,在上面的测试用例中,我们传递了8个数值用来验证MathTools的isEven方法。
这种方式虽然已经比较好了,但是我们还是得需要输入所有我们想要测试的值。那如果我们想要测试0-1000的所有数值该怎么办呢?相较于手敲500个数字,我们可以将@ValueSource注解换成==@MethodSource==注解,它可以为我们生成数值列表,如下所示:
@ParameterizedTest
@MethodSource("generateEvenNumbers")
void testIsEvenRange(int number) {
Assertions.assertTrue(MathTools.isEven(number));
}
static IntStream generateEvenNumbers() {
return IntStream.iterate(0, i -> i + 2).limit(500);
}
参数化测试 · 源
参数化测试包括对以下类型的源的支持:
- ValueSource: 制定手打的数值或字符串列表。
- MethodSource: 调用静态方法,该方法生成项流或项集合。
- EnumSource:指定枚举,其值将被传递给测试方法。它允许您遍历所有枚举值,或包含或排除特定的枚举值。
- CsvSource: 指定以逗号分隔的值列表。
- CsvFileSource:指定带测试数据的逗号分隔值文件的路径。
- ArgumentSource:允许指定一个参数提供程序,该提供程序生成要传递给测试方法的参数流。
- NullSource:如果使用的是字符串、集合或数组,则将null传递给测试方法。可以将此注释与其他注释(如ValueSource)一起包含,以编写测试值和null集合的代码。
- EmptySource: 如果使用的是字符串、集合或数组,则应包含一个空值。
- NullAndEmptySource:如果使用的是字符串、集合或数组,则同时包含空值和空值。
五、断言库
大多数情况下,默认的断言库可以满足需求,但如果你想要的使用更健壮的断言库,比如AssertJ, Hamcrest, or Truth,JUnit 5 也提供支持。
Hamcrest
简介
Hamcrest基于匹配器的概念,这是判断测试结果是否处于期望状态的一种非常自然的方法。
它是如何工作的?
Step1 添加依赖
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
Step2 Demo
接下来,当我们想要在测试类中使用Hamcrest时,我们需要利用org.hamcrest.MatcherAssert.assertThat方法,该方法与一个或多个它的匹配器结合使用。例如,String是否相等的测试可能像这样:
assertThat(name, is("Steve"));
// 或者
assertThat(name, equalsTo("Steve"));
// is()与equalsTo() 没什么区别, 前者就是后者的语法糖
Hamcrest 匹配器:
- Objects:
equalTo
,hasToString
,instanceOf
,isCompatibleType
,notNullValue
,nullValue
,sameInstance
- Text:
equalToIgnoringCase
,equalToIgnoringWhiteSpace
,containsString
,endsWith
,startsWith
- Numbers:
closeTo
,greaterThan
,greaterThanOrEqualTo
,lessThan
,lessThanOrEqualTo
- Logical:
allOf
,anyOf
,not
- Collections:
array
(compare an array to an array of matchers),hasEntry
,hasKey
,hasValue
,hasItem
,hasItems
,hasItemInArray
Demo:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
class HamcrestDemoTest {
@Test
@DisplayName("String Examples")
void stringExamples() {
String s1 = "Hello";
String s2 = "Hello";
assertThat("Comparing Strings", s1, is(s2));
assertThat(s1, equalTo(s2));
assertThat(s1, sameInstance(s2));
assertThat("ABCDE", containsString("BC"));
assertThat("ABCDE", not(containsString("EF")));
}
@Test
@DisplayName("List Examples")
void listExamples() {
// Create an empty list
List<String> list = new ArrayList<>();
assertThat(list, isA(List.class));
assertThat(list, empty());
// Add a couple items
list.add("One");
list.add("Two");
assertThat(list, not(empty()));
assertThat(list, hasSize(2));
assertThat(list, contains("One", "Two"));
assertThat(list, containsInAnyOrder("Two", "One"));
assertThat(list, hasItem("Two"));
}
@Test
@DisplayName("Number Examples")
void numberExamples() {
assertThat(5, lessThan(10));
assertThat(5, lessThanOrEqualTo(5));
assertThat(5.01, closeTo(5.0, 0.01));
}
}
Hamcrest 官网
If you’re new to Hamcrest, I encourage you to learn more about it from the Hamcrest website.
六、JUnit 5 test 的生命周期
简介
对于许多测试,您可能希望在每次测试运行之前和之后以及在所有测试运行之前和之后做一些事情。例如,如果你是测试数据库查询,您可能想要建立一个连接到一个数据库并导入模式运行了所有的测试之前,插入测试数据在每个测试运行之前,清理数据库每次测试运行后,然后删除模式和所有的测试运行后关闭数据库连接。
注解
JUnit 5提供了以下注释,你可以在测试类的方法中添加这些注释:
@BeforeAll
: A static method in your test class that is called before all of its tests run.@AfterAll
: A static method in your test class that is called after all of its tests run.@BeforeEach
: A method that is called before each individual test runs.@AfterEach
: A method that is called after each individual test runs.
Demo
import org.junit.jupiter.api.*;
public class LifecycleDemoTest {
@BeforeAll
static void beforeAll() {
System.out.println("Connect to the database");
}
@BeforeEach
void beforeEach() {
System.out.println("Load the schema");
}
@AfterEach
void afterEach() {
System.out.println("Drop the schema");
}
@AfterAll
static void afterAll() {
System.out.println("Disconnect from the database");
}
@Test
void testOne() {
System.out.println("Test One");
}
@Test
void testTwo() {
System.out.println("Test Two");
}
}
七、JUnit 5 新特性 - Tags
简介
在结束对JUnit 5核心的介绍之前,我将向您展示如何使用标记在不同的场景中有选择地运行不同的测试用例。标记用于识别和筛选希望在不同场景中运行的特定测试。例如,您可以将一个测试类或测试方法标记为集成测试,将另一个标记为开发。标签的名称和用途完全由您决定。
Demo
我们将创建三个新的测试类,并将其中两个标记为开发类,另一个标记为生产类,这大概是为了在为不同环境构建时区分您想要运行的测试。
Test 1 (TestOne.java)
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("Development")
class TestOne {
@Test
void testOne() {
System.out.println("Test 1");
}
}
Test 2 (TestTwo.java)
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("Development")
class TestTwo {
@Test
void testTwo() {
System.out.println("Test 2");
}
}
Test 3 (TestThree.java)
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("Production")
class TestThree {
@Test
void testThree() {
System.out.println("Test 3");
}
}
标签是通过注释实现的,您可以注释整个测试类,也可以注释测试类中的单个方法;此外,一个类或方法可以有多个标记。在这个例子中,teststone和TestTwo用“Development”标签标注,TestThree用“Production”标签标注。我们可以基于标签以不同的方式过滤测试运行。最简单的方法是在Maven命令行中指定一个测试;例如,以下只执行标记为“开发”的测试:
mvn clean test -Dgroups="Development"
mvn clean test -Dgroups="Production"
mvn clean test -Dgroups="Development, Production"
mvn clean test -DexcludedGroups="Production"
详细教程
I encourage you to review the JUnit 5 User Guide to learn more about tags.
八、使用Mockito Mock objects
引言
到目前为止,我们只回顾了不依赖于外部依赖的简单方法的测试,但这对于大型应用程序来说是很不正常的。例如,业务服务可能依赖于数据库或web服务调用来检索它所操作的数据。那么我们如何在这样的类中测试方法呢?我们如何模拟有问题的情况,比如数据库连接错误或超时?
简介
模拟对象的策略是分析被测试的类,并创建其所有依赖项的模拟版本,从而创建我们想要测试的场景。您可以手动完成这项工作(工作量很大),也可以利用Mockito这样的工具,它简化了模拟对象的创建和类的注入。Mockito提供了一个简单的API来创建依赖类的模拟实现、将模拟注入到类中并控制模拟的行为。
Demo
如下示例显示了一个简单存储库的源代码:
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
public class Repository {
public List<String> getStuff() throws SQLException {
// Execute Query
// Return results
return Arrays.asList("One", "Two", "Three");
}
}
如下示例显示了使用上述方法的服务的源代码:
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Service {
private Repository repository;
public Service(Repository repository) {
this.repository = repository;
}
public List<String> getStuffWithLengthLessThanFive() {
try {
return repository.getStuff().stream()
.filter(stuff -> stuff.length() < 5)
.collect(Collectors.toList());
} catch (SQLException e) {
return Arrays.asList();
}
}
}
Nice analysis
笔者非常喜欢官网对上述代码的分析,因此原封不动的保留了下来:
The Repository in Listing 9 has a single method, getStuff, that would presumably connect to a database, execute a query, and return the results. In this example, it simply returns a list of three Strings. The Service in Listing 10 receives the Repository through its constructor and defines a single method, getStuffWithLengthLessThanFive, which returns all Strings with a length less than 5. If the repository throws a SQLException then it returns an empty list.
用JUnit 5和Mockito单元测试
接下来,就是见证奇迹的时刻!
Demo
如下示例显示了ServiceTest类的源代码:
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock
Repository repository;
@InjectMocks
Service service;
@Test
void testSuccess() {
// Setup mock scenario
try {
Mockito.when(repository.getStuff()).thenReturn(Arrays.asList("A", "B", "CDEFGHIJK", "12345", "1234"));
} catch (SQLException e) {
e.printStackTrace();
}
// Execute the service that uses the mocked repository
List<String> stuff = service.getStuffWithLengthLessThanFive();
// Validate the response
Assertions.assertNotNull(stuff);
Assertions.assertEquals(3, stuff.size());
}
@Test
void testException() {
// Setup mock scenario
try {
Mockito.when(repository.getStuff()).thenThrow(new SQLException("Connection Exception"));
} catch (SQLException e) {
e.printStackTrace();
}
// Execute the service that uses the mocked repository
List<String> stuff = service.getStuffWithLengthLessThanFive();
// Validate the response
Assertions.assertNotNull(stuff);
Assertions.assertEquals(0, stuff.size());
}
}
分析
- @ExtendWith注释用于加载JUnit 5扩展。
- JUnit定义了一个扩展API,它允许像Mockito这样的第三方供应商挂钩到运行测试类的生命周期中,并添加额外的功能。
- MockitoExtension查看测试类,找到带有@Mock注释的成员变量,并创建这些变量的模拟实现。
- 然后,它找到带有@InjectMocks注释的成员变量,并尝试使用构造注入或setter注入将其模拟注入到这些类中。
- 在这个例子中
- MockitoExtension在Repository成员变量上找到@Mock注释,因此它创建它的模拟实现,并将它分配给存储库变量
- 当它看到Service成员变量上的@ injectmock注释时,它创建Service类的一个实例,并将模拟Repository传递给它的构造函数。
- 它允许我们使用Mockito的api控制模拟Repository类的行为。
- 在testSuccess方法中,我们使用Mockito API在调用getStuff方法时返回一个特定的结果集。
- API的工作方式如下:当Mockito::定义条件时,在本例中是调用repository.getStuff()方法
- when()方法返回一个org.mockito.stubbing.OngoingStubbing实例,该实例定义了一组方法,这些方法决定当指定的方法被调用时该做什么。在本例中,我们调用thenReturn()方法来告诉存根返回一个特定的string列表。
- 此时,我们有了一个带有模拟存储库的Service实例。
- 当调用Repository的getStuff方法时,它返回一个由5个已知字符串组成的列表。
- 我们调用服务的getStuffWithLengthLessThanFive()方法,该方法将调用Repository的getStuff()方法,并返回一个长度小于5的字符串筛选列表。
- 然后我们可以断言返回的列表不是空的,并且它的大小是3。
- 这个过程允许我们在特定的Service方法中测试逻辑,使用来自Repository的已知响应。
- testException方法配置Mockito,以便当调用Repository的getStuff()方法时,它会抛出一个SQLException
- 如果发生这种情况,服务不应该抛出异常;相反,它应该返回一个空列表。
PowerMock
Mockito是一个强大的工具,我们对它的功能只了解了皮毛。如果您想知道如何测试不一致的条件(如网络、数据库、超时或其他I/O错误条件),那么mockito就是适合您的工具,它与JUnit 5一起工作非常出色。如果您遇到Mockito不支持的情况,比如模拟静态成员变量或私有构造函数,那么还有另一个功能强大但复杂的工具PowerMock.
总结
在本文中,我快速地向您介绍了使用JUnit 5的一些亮点。我向您展示了如何配置Maven项目以使用JUnit 5,以及如何使用@Test和@ParameterizedTest注释编写测试。然后介绍了大多数JUnit 5生命周期注释,回顾了过滤器标记的使用和好处,并介绍了JUnit 5与Hamcrest的集成。最后,我介绍了Mockito,并演示了如何使用模拟对象来测试一些更健壮的Java类和场景。
在本文的第二部分中,我们将在此基础上进行构建,在这里您将学习如何将JUnit 5与Spring框架集成在一起。您将学习如何使用JUnit 5的内置和第三方类和扩展来测试Spring web控制器、服务和存储库。
参考文献
函数,那么还有另一个功能强大但复杂的工具PowerMock.
总结
在本文中,我快速地向您介绍了使用JUnit 5的一些亮点。我向您展示了如何配置Maven项目以使用JUnit 5,以及如何使用@Test和@ParameterizedTest注释编写测试。然后介绍了大多数JUnit 5生命周期注释,回顾了过滤器标记的使用和好处,并介绍了JUnit 5与Hamcrest的集成。最后,我介绍了Mockito,并演示了如何使用模拟对象来测试一些更健壮的Java类和场景。
在本文的第二部分中,我们将在此基础上进行构建,在这里您将学习如何将JUnit 5与Spring框架集成在一起。您将学习如何使用JUnit 5的内置和第三方类和扩展来测试Spring web控制器、服务和存储库。