QA之二 - 单元测试--JUnit5
JUnit5
一、是什么?
JUnit 5 是 Java 生态最新的单元测试框架(替代 JUnit 4),全称「JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage」,专为 Java 8 + 设计,支持 Lambda 表达式、流式 API,适配现代 Java 开发。
三大模块:
- JUnit Platform:测试运行的基础平台(启动测试、对接 IDE / 构建工具如 Maven/Gradle);
- JUnit Jupiter:核心模块:定义 JUnit 5 的测试注解、断言、测试接口等(写测试用例的核心);
- JUnit Vintage:兼容 JUnit 4/3 的测试用例(过渡用);
二、核心注解
JUnit 5 的注解全部在org.junit.jupiter.api包下,以下是开发中最常用的:
- @Test:标记方法为测试方法(无需public,可直接用void/ 有返回值);
- @DisplayName:自定义测试类 / 方法的显示名称(易读,如中文描述);
- @BeforeEach:每个测试方法执行前运行(初始化测试数据 / 对象),如初始化多签服务实例、模拟数据库连接;
- @AfterEach:每个测试方法执行后运行(清理资源),如关闭模拟连接、重置测试状态;
- @BeforeAll:所有测试方法执行前运行一次(静态方法),如加载配置文件、初始化全局测试资源;
- @AfterAll:所有测试方法执行后运行一次(静态方法),如释放全局资源、关闭连接池;
- @Disabled:禁用某个测试方法 / 类(临时跳过),如@Disabled("暂不测试,待修复bug");
- @Nested:嵌套测试(按业务逻辑分组测试,如 “多签门限测试” 嵌套在 “多签服务测试” 下),结构化管理测试用例,更清晰;
- @Tag:给测试打标签(按标签执行测试,如@Tag("core")/@Tag("unit"),区分 “核心逻辑测试” 和 “非核心逻辑测试”;
关键注意(和 JUnit4 的区别)
- JUnit5:@BeforeEach/@AfterEach/@BeforeAll/@AfterAll(去掉了 JUnit 4 的@Before/@After);
- @Test 无需public修饰,JUnit 5 更简洁;
- 测试生命周期
-
PER_METHOD:默认模式,每个测试方法创建一个实例;
- 生命周期流程:@BeforeAll(全局初始化) --> 创建测试实例1 --> @BeforeEach(方法1前置) --> @Test 方法1 --> @AfterEach(方法1后置) --> 创建测试实例2"] --> @BeforeEach(方法2前置) --> @Test 方法2 --> @AfterEach(方法2后置) --> @AfterAll(全局清理)
-
PER_CLASS:需给测试类加@TestInstance(TestInstance.Lifecycle.PER_CLASS),测试实例仅创建一次;
- @BeforeAll/@AfterAll可非静态;
- 生命周期流程:1. 初始化测试类(仅1次) --> 2. 执行@BeforeAll(非静态,仅1次) --> 3. 执行@BeforeEach(测试方法1前置) --> 4. 执行@Test 方法1 --> 5. 执行@AfterEach(测试方法1后置) --> 6. 执行@BeforeEach(测试方法2前置) --> 7. 执行@Test 方法2 --> 8. 执行@AfterEach(测试方法2后置) --> 9. 执行@AfterAll(非静态,仅1次) --> 10. 销毁测试实例(仅1次)
-
三、关键断言
JUnit 5 的断言在org.junit.jupiter.api.Assertions类中,支持 Lambda 表达式、异常断言、超时断言,核心用法如下:
(1)基础断言(最常用)
点击查看代码
import static org.junit.jupiter.api.Assertions.*;
@Test
@DisplayName("测试多签门限校验-基础断言")
void testSignatureThreshold() {
// 1. 相等断言(核心)
int actual = multisigService.checkSignatureCount(2); // 实际结果
int expected = 2; // 预期结果
assertEquals(expected, actual, "签名数校验结果不匹配"); // 支持自定义错误信息
// 2. 布尔断言
boolean isValid = multisigService.isValidThreshold(2);
assertTrue(isValid, "门限=2时应返回true");
assertFalse(multisigService.isValidThreshold(1), "门限=1时应返回false");
// 3. 空值断言
String txId = multisigService.getTxId(1);
assertNotNull(txId, "交易ID不应为空");
assertNull(multisigService.getTxId(-1), "无效ID应返回null");
// 4. 相同引用断言(判断对象是否是同一个)
assertSame(multisigService, multisigService, "对象引用应相同");
}
(2)异常断言(测试异常场景,如多签签名数不足)
点击查看代码
@Test
@DisplayName("测试签名数不足时抛出异常")
void testSignatureInsufficient() {
// 断言执行lambda表达式时会抛出指定异常
assertThrows(IllegalArgumentException.class,
() -> multisigService.executeTx(1), // 执行会抛异常的方法
"签名数不足时应抛出IllegalArgumentException");
// 进阶:获取异常对象,验证异常信息
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> multisigService.executeTx(1));
assertEquals("签名数不足(需≥2)", exception.getMessage());
}
(3)超时断言(测试方法执行超时,如时间锁计算)
点击查看代码
@Test
@DisplayName("测试时间锁到期判断不超时")
void testTimelockCheckTimeout() {
// 断言方法执行时间≤100ms
assertTimeout(Duration.ofMillis(100),
() -> timelockService.isTimeLockPassed(1),
"时间锁校验应在100ms内完成");
}
(4)组合断言(批量验证,失败时全部展示)
点击查看代码
@Test
@DisplayName("组合断言验证多签交易状态")
void testTxStatus() {
// 所有断言都执行,失败的会全部列出(而非第一个失败就停止)
assertAll("交易状态校验",
() -> assertEquals(2, multisigService.getSignatureCount(1)),
() -> assertTrue(multisigService.isTxPending(1)),
() -> assertFalse(multisigService.isTxExecuted(1))
);
}
四、高阶特性
- 参数化测试(批量测试多场景)
核心解决:重复写 “输入不同参数、验证不同结果” 的测试用例(如多签门限 = 1/2/3),用@ParameterizedTest实现,需依赖junit-jupiter-params包。
(1)基础参数化(@ValueSource)
点击查看代码
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
// 测试多签门限的多个场景:1(无效)、2(有效)、3(有效)
@ParameterizedTest
@ValueSource(ints = {1, 2, 3}) // 传入int类型参数列表
@DisplayName("参数化测试多签门限有效性")
void testThresholdValid(int threshold) {
boolean result = multisigService.isValidThreshold(threshold);
// 断言:threshold=1时false,2/3时true
if (threshold == 1) {
assertFalse(result);
} else {
assertTrue(result);
}
}
(2)多参数测试(@CsvSource)
点击查看代码
@ParameterizedTest
@CsvSource({
"0, false", // 签名数=0 → 无效
"1, false", // 签名数=1 → 无效
"2, true", // 签名数=2 → 有效
"3, true" // 签名数=3 → 有效
})
@DisplayName("多参数测试签名数有效性")
void testSignatureCountValid(int count, boolean expected) {
boolean actual = multisigService.isValidSignatureCount(count);
assertEquals(expected, actual, "签名数=" + count + "时结果错误");
}
(3)方法来源参数(@MethodSource)
点击查看代码
// 1. 定义参数生成方法(静态,返回Stream)
static Stream<Arguments> signatureData() {
return Stream.of(
Arguments.of(0, false),
Arguments.of(2, true),
Arguments.of(5, true)
);
}
// 2. 参数化测试引用该方法
@ParameterizedTest
@MethodSource("signatureData")
@DisplayName("方法来源测试签名数")
void testSignatureFromMethod(int count, boolean expected) {
assertEquals(expected, multisigService.isValidSignatureCount(count));
}
- 嵌套测试(@Nested)
按业务逻辑分组测试,让测试用例更结构化(如 “多签服务” 下分 “门限测试”“执行测试”):
点击查看代码
@DisplayName("多签服务单元测试")
class MultisigServiceTest {
private MultisigService multisigService;
@BeforeEach
void init() {
multisigService = new MultisigService(2); // 初始化门限=2
}
// 嵌套测试:门限相关测试
@Nested
@DisplayName("门限校验模块")
class ThresholdTest {
@Test
@DisplayName("门限=2时校验通过")
void testThreshold2() {
assertTrue(multisigService.isValidThreshold(2));
}
@Test
@DisplayName("门限=1时校验失败")
void testThreshold1() {
assertFalse(multisigService.isValidThreshold(1));
}
}
// 嵌套测试:执行交易模块
@Nested
@DisplayName("执行交易模块")
class ExecuteTxTest {
@Test
@DisplayName("签名数=2时执行成功")
void testExecuteSuccess() {
assertTrue(multisigService.executeTx(1));
}
@Test
@DisplayName("签名数=1时执行失败")
void testExecuteFail() {
assertThrows(IllegalArgumentException.class, () -> multisigService.executeTx(2));
}
}
}
五、实战
- 第一步:引入依赖(Maven)
点击查看代码
<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>org.example</groupId>
<artifactId>Junit5</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- 统一指定项目编码为 UTF-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一指定项目报告编码(可选,避免报告乱码) -->
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- JUnit 5 核心依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- 参数化测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- 执行快速、独立的单元测试(失败会直接中断构建)-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<!-- 专门用于执行集成测试(Integration Tests) -->
<!--<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>-->
</plugins>
</build>
</project>
- 被测类(多签服务)
点击查看代码
package com.example;
public class MultisigService {
// 多签门限(默认2)
private int threshold;
public MultisigService(int threshold) {
this.threshold = threshold;
}
public int checkSignatureCount(){
return threshold;
}
// 校验签名数是否达标
public boolean isValidThreshold(int count) {
return count >= threshold;
}
public boolean isTxPending(int num){
return num == 1;
}
public boolean isTxExecuted(int num){
return num != 1;
}
// 执行交易(签名数不足抛异常)
public boolean executeTx(int txId) {
int signatureCount = getSignatureCount(txId);
if (!isValidThreshold(signatureCount)) {
throw new IllegalArgumentException("签名数不足(需≥" + threshold + ")");
}
// 模拟执行交易
return true;
}
public void isTimeLockPassed(long time){
try {
Thread.sleep( time );
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public String getTxId(int nounce){
return "txId-" + nounce;
}
// 模拟获取签名数(测试用)
private int getSignatureCount(int txId) {
// txId=1返回2,其他返回1
return txId == 1 ? 2 : 1;
}
}
- JUnit 5 测试类
点击查看代码
// MultisigServiceTest.java(测试类)
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
// 全局显示名称
@DisplayName("多签服务单元测试(JUnit 5)")
// 测试实例仅创建一次(@BeforeAll可非静态)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MultisigServiceTest {
private MultisigService multisigService;
// 全局初始化(所有测试前执行一次)
@BeforeAll
@DisplayName("初始化多签服务")
void initGlobal() {
System.out.println("===== 全局初始化 =====");
}
// 每个测试方法前初始化
@BeforeEach
@DisplayName("初始化测试数据")
void init() {
multisigService = new MultisigService(2); // 门限=2
System.out.println("----- 测试方法初始化 -----");
}
// 基础测试:签名数=2时有效
@Test
@DisplayName("测试签名数=2时校验通过")
void testValidSignatureCount() {
boolean result = multisigService.isValidSignatureCount(2);
assertTrue(result, "签名数=2应返回true");
}
// 异常测试:签名数不足抛异常
@Test
@DisplayName("测试签名数=1时执行交易抛异常")
void testExecuteTxWithInsufficientSignature() {
// 断言抛异常,且异常信息正确
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> multisigService.executeTx(2), // txId=2 → 签名数=1
"签名数不足时应抛IllegalArgumentException");
assertEquals("签名数不足(需≥2)", exception.getMessage());
}
// 参数化测试:多场景校验签名数
@ParameterizedTest
@CsvSource({"0,false", "1,false", "2,true", "3,true"})
@DisplayName("参数化测试-签名数有效性")
void testSignatureCountParam(int count, boolean expected) {
boolean actual = multisigService.isValidSignatureCount(count);
assertEquals(expected, actual, "签名数=" + count + "时结果错误");
}
// 嵌套测试:执行交易模块
@Nested
@DisplayName("执行交易子模块测试")
class ExecuteTxSubTest {
@Test
@DisplayName("txId=1时执行成功(签名数=2)")
void testExecuteSuccess() {
assertTrue(multisigService.executeTx(1));
}
@Test
@DisplayName("txId=2时执行失败(签名数=1)")
void testExecuteFail() {
assertThrows(IllegalArgumentException.class, () -> multisigService.executeTx(2));
}
}
// 禁用测试(临时跳过)
@Test
@Disabled("暂不测试,待优化逻辑")
@DisplayName("禁用测试-时间锁集成")
void testTimelockIntegration() {
// 待实现
}
// 每个测试方法后清理
@AfterEach
@DisplayName("清理测试数据")
void clean() {
System.out.println("----- 测试方法清理 -----");
}
// 全局清理(所有测试后执行一次)
@AfterAll
@DisplayName("全局清理")
void cleanGlobal() {
System.out.println("===== 全局清理 =====");
}
}
- 运行测试(IDEA/Maven)
IDEA:直接右键测试类→Run MultisigServiceTest,可看到带中文的测试名称,清晰易读;
Maven:执行命令 mvn test -Dtest=测试类名 ,Maven Surefire 插件会自动运行 JUnit 5 测试。执行 MultisigServiceTest 单个测试类(无需写.java后缀):
mvn test -Dtest=MultisigServiceTest
执行结果:
点击查看
===== 全局初始化 =====
暂不测试,待优化逻辑
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
===== 全局清理 =====
学习技术不是用来写HelloWorld和Demo的,而是要用来解决线上系统的真实问题的.

浙公网安备 33010602011771号