在我们的日常开发中,代码一边编码一边自测是常有的事,做好单元测试也是一名开发应该掌握的技能,不说测试搞得多么强,至少会基本的,会功能测试,会性能测试。今天来学习下 单元测试。
1.JUnit5介绍
现在主要版本是 JUnit5,所以后面的内容也都是基于 JUnit5 做相关的介绍。JUnit5 是 JUnit 单元测试框架的重大升级,需要运行在 Java8 以上的环境。
JUnit5可以理解为是由三个不同而子项目构成:
- 1.JUnit Platform,用于JVM上启动测试框架的基础服务,提供命令行,IDE和构建工具等方式执行测试的支持。
- 2.JUnit Jupiter,包含 JUnit 5 新的编程模型和扩展模型,主要就是用于编写测试代码和扩展代码。
- 3.JUnit Vintage,用于在JUnit 5 中兼容运行 JUnit3.x 和 JUnit4.x 的测试用例。
JUnit5目前的主要特性:
- 提供全新的断言和测试注解,支持测试类内嵌
- 更丰富的测试方式:支持动态测试,重复测试,参数化测试等
- 实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖
- 提供对 Java 8 的支持,如 Lambda 表达式,Sream API等。
2.测试环境
- Java 21
- Junit 5.10.0
3.Maven依赖
本篇学习内容涉及的依赖如下:
xml
<!-- 基础测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
</dependency>
<!-- 带参测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.0</version>
</dependency>
4.典型注解
@BeforeAll
修饰 static 方法,定义整个测试类在开始前的操作,比如一些初始化的操作。
@AfterAll
修饰 static 方法,定义整个测试类在结束时的操作,比如一些清理工作。
@BeforeEach && @AfterEach
标注在每个测试用例方法,表示方法开始前或结束时的执行,负责该测试用例所需要的的运行环境的准备和销毁。
@Test
表示这个测试方法是一个测试用例。
由上面的这些注解,我们可以做出如下流程来表示单元测试的完整过程:
@DisplayName
可以加在类上,也可以加在测试方法上,相当于一个显示名。
@Disable
相当于禁用当前的测试方法,测试时就会忽略该方法。
@RepeatedTest
表示该方法需要重复运行,具体几次,可以通过参数传入。
基于上面的介绍,我们写一个简单的类测试:
Java
package org.example;
import org.junit.jupiter.api.*;
import java.time.Duration;
@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
public void testFirstTest() {
System.out.println("我的第一个测试开始");
}
@DisplayName("我的第二个测试")
@Test
public void testSecondTest() {
System.out.println("我的第二个测试开始");
}
@DisplayName("我的第三个测试")
@Disabled
@Test
public void testThirdTest() {
System.out.println("我的第三个测试开始测试");
}
@DisplayName("我的第四个测试-重复测试")
@RepeatedTest(value = 3, name = "{displayName} 第 {currentRepetition} 次")
public void repeatedTest() {
System.out.println("正在执行重复测试");
}
}
5.断言
在测试方法中,我们常常是给定一个预期的值和测试的结果值作比较,看对应的结果怎样,由此就由相关断言:
- 断言相等,assertEqual
- 断言不等,assertNotEqual
- 多个断言,assertAll
- 断言空或非空,assertNull/assertNotNull
- 断言 true或false,assertTrue/assertFalse
- 超时断言,assertTimeout/assertTimeoutPreemptively
- 断言实例,assertInstanceOf
- 断言异常,assertThrows
由于断言类包含内容很多,每个方法实际很多重载,这里仅挑选几个重点说说,其他都是类似的。
demo
Java
@DisplayName("我的第五个测试-单个断言")
@Test
public void testSingleAssertion() {
Integer num = 1;
Assertions.assertEquals(num, 1);
}
@DisplayName("我的第六个测试-多断言")
@Test
public void testGroupAssertions() {
int[] nums = {0, 1, 2, 3, 4};
Assertions.assertAll("nums",
() -> Assertions.assertEquals(nums[0], 0),
() -> Assertions.assertEquals(nums[1], 1),
() -> Assertions.assertEquals(nums[2], 2),
() -> Assertions.assertEquals(nums[3], 3),
() -> Assertions.assertEquals(nums[4], 4)
);
}
@DisplayName("我的第七个测试-超时操作")
@Test
public void testShouldCompleteInOneSecond() {
// 无法做到时间的精确匹配
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> Thread.sleep(999));
}
@DisplayName("我的第八个测试-异常测试")
@Test
public void testAssertThrowsException() {
String str = null;
// str 作为传入参数,会报非法传参异常,所以可以正常断言到
Assertions.assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(str);
});
}
6.带参数的测试方法
有的时候我们需要一些带参数的测试方法,比如一次测试一个参数不够,那就来一组参数,或者有的传参需要多个参数怎么办。
这里我们用到了 Junit5的 params 包提供功能,这也是上面我们在依赖中添加的依赖项。
在有带参数的单元测试中主要介绍几个常用的,其他感兴趣可以看看源码。
@ParameterizedTest
此处可以用来代替 @Test 注解,一样的功效。
@ValueSource
指定我们的传入的这一组参数,可以是:
- ints
- strings
- classes等
包含类型覆盖基本类型。
@CsvSource
规定了传入的多个入参的组合形式,默认用","分隔。
下面是个简单的 demo:
Java
package org.example;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
public class ParameterUnitTest {
@ParameterizedTest
@ValueSource(ints = {2, 4, 8, 10})
public void testIsEvenNumber(int num) {
Assertions.assertEquals(0, num % 2);
}
@ParameterizedTest
@ValueSource(strings = {"Effective Java", "C Plus Plus"})
public void testPrintTitle(String title) {
System.out.println(title);
}
// 多参数
@ParameterizedTest
@CsvSource({"1,One", "2,Two"})
public void testDataFromCSV(long id, String name) {
System.out.printf("id: %d, name: %s\n", id, name);
}
}
以上这些都是一些基本用法,有的时候我们需要依赖其他类,所以就会有 mock打桩 的需求,这是后话了,掌握这些基本用法能覆盖相当的开发自测了。
参考: