QA之二 - 单元测试-- JUnit5 + Mockito
单元测试是针对软件中最小的可测试单元(如单个函数、方法、合约函数)进行的独立测试,目的是验证这个单元的逻辑是否完全符合预期,且仅关注该单元本身,不依赖外部模块 / 服务。
通俗解释:把复杂的系统拆成最小的 “零件” 逐一测试:比如测试一辆车:单元测试不测试 “整车行驶”,只测试 “单个刹车卡钳”“单个发动机气缸” 是否正常工作。
Java开发中最常用的单元测试组合:JUnit5 + Mockito 。
一、JUnit 5 + Mockito
JUnit 5 与 Mockito 结合使用 —— 这是 Java 单元测试的黄金组合,核心是用 Mockito 隔离外部依赖,用 JUnit 5 管理测试生命周期和断言。
二、整合步骤
- 第一步:引入依赖(Maven)
需同时引入 JUnit 5 核心依赖、Mockito 核心依赖,以及 Mockito 适配 JUnit 5 的扩展包:
点击查看代码
<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>11</maven.compiler.source>
<maven.compiler.target>11</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>
<!-- Mockito 核心依赖 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito 适配 JUnit 5 的扩展 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- 可选:静态方法Mock依赖(如需Mock静态工具类) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</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>
- 核心整合注解
JUnit 5 中启用 Mockito 只需在测试类上加 @ExtendWith(MockitoExtension.class)
基础示例(核心分工演示)
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
// 核心:启用Mockito对JUnit 5的扩展
@ExtendWith(MockitoExtension.class)
@DisplayName("JUnit 5 + Mockito 基础示例")
class BasicIntegrationTest {
// ========== 1. Mockito:创建Mock对象(模拟外部依赖) ==========
@Mock
private RpcClient rpcClient; // 模拟链上RPC客户端
// ========== 2. Mockito:将Mock对象注入被测对象 ==========
@InjectMocks
private MultisigService multisigService; // 被测的多签服务
// ========== 3. JUnit 5:测试生命周期初始化 ==========
@BeforeEach
void setUp() {
// 可选:手动补充初始化逻辑(Mockito已自动创建Mock对象)
System.out.println("测试方法前置初始化");
}
// ========== 4. 核心测试方法(JUnit 5 + Mockito 结合) ==========
@Test
@DisplayName("测试多签交易执行成功(RPC返回正常)")
void testExecuteMultisigTxSuccess() {
// --- Mockito:定制Mock行为 ---
// 当调用rpcClient.sendTx("tx-001")时,返回true
when(rpcClient.sendTx("tx-001")).thenReturn(true);
// --- JUnit 5:执行被测方法 + 断言结果 ---
boolean result = multisigService.executeTx("tx-001");
assertTrue(result, "多签交易执行应返回成功");
// --- Mockito:验证Mock交互行为 ---
// 验证rpcClient.sendTx("tx-001")被调用了1次
verify(rpcClient, times(1)).sendTx("tx-001");
// 验证rpcClient其他方法未被调用
verifyNoMoreInteractions(rpcClient);
}
@Test
@DisplayName("测试多签交易执行失败(RPC抛出异常)")
void testExecuteMultisigTxFail() {
// --- Mockito:定制Mock抛出异常 ---
when(rpcClient.sendTx("tx-002")).thenThrow(new RuntimeException("RPC超时"));
// --- JUnit 5:执行 + 断言 ---
boolean result = multisigService.executeTx("tx-002");
assertFalse(result, "RPC异常时交易应执行失败");
// --- Mockito:验证交互 ---
verify(rpcClient, times(1)).sendTx("tx-002");
}
// ========== 5. JUnit 5:测试生命周期清理 ==========
@AfterEach
void tearDown() {
// 可选:重置Mock对象(避免多方法间Mock行为污染)
reset(rpcClient);
}
}
// 模拟依赖的RPC客户端接口
interface RpcClient {
boolean sendTx(String txId);
}
// 被测的多签服务类
class MultisigService {
private RpcClient rpcClient;
// Mockito会自动通过构造方法注入Mock的rpcClient
public MultisigService(RpcClient rpcClient) {
this.rpcClient = rpcClient;
}
public boolean executeTx(String txId) {
try {
return rpcClient.sendTx(txId);
} catch (Exception e) {
return false;
}
}
}
三、进阶用法
- 参数化测试 + Mockito(批量测试多场景)
点击查看代码
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
// 延续上面的测试类,新增参数化测试方法
@ExtendWith(MockitoExtension.class)
class ParameterizedIntegrationTest {
@Mock
private RpcClient rpcClient;
@InjectMocks
private MultisigService multisigService;
// JUnit 5参数化测试:批量测试不同txId和RPC返回值的场景
@ParameterizedTest
@CsvSource({
"tx-001, true, true", // txId=001, RPC返回true → 交易成功
"tx-002, false, false", // txId=002, RPC返回false → 交易失败
"tx-003, exception, false" // txId=003, RPC抛异常 → 交易失败
})
@DisplayName("参数化测试:多签交易执行结果")
void testExecuteTxWithParams(String txId, String rpcBehavior, boolean expectedResult) {
// --- Mockito:根据参数定制Mock行为 ---
if ("true".equals(rpcBehavior)) {
when(rpcClient.sendTx(txId)).thenReturn(true);
} else if ("false".equals(rpcBehavior)) {
when(rpcClient.sendTx(txId)).thenReturn(false);
} else if ("exception".equals(rpcBehavior)) {
when(rpcClient.sendTx(txId)).thenThrow(new RuntimeException("异常"));
}
// --- JUnit 5:执行 + 断言 ---
boolean actualResult = multisigService.executeTx(txId);
assertEquals(expectedResult, actualResult, "txId=" + txId + "时结果错误");
// --- Mockito:验证交互 ---
verify(rpcClient, times(1)).sendTx(txId);
}
}
- 嵌套测试 + Mockito(按业务逻辑分组)
点击查看代码
@ExtendWith(MockitoExtension.class)
@DisplayName("嵌套测试:多签服务全场景")
class NestedIntegrationTest {
@Mock
private RpcClient rpcClient;
@InjectMocks
private MultisigService multisigService;
// JUnit 5嵌套测试:分组测试“成功场景”
@Nested
@DisplayName("交易执行成功场景")
class SuccessScenarios {
@Test
@DisplayName("RPC返回true时成功")
void testSuccess() {
when(rpcClient.sendTx("tx-001")).thenReturn(true);
assertTrue(multisigService.executeTx("tx-001"));
}
}
// 嵌套测试:分组测试“失败场景”
@Nested
@DisplayName("交易执行失败场景")
class FailScenarios {
@Test
@DisplayName("RPC返回false时失败")
void testFail_RpcFalse() {
when(rpcClient.sendTx("tx-002")).thenReturn(false);
assertFalse(multisigService.executeTx("tx-002"));
}
@Test
@DisplayName("RPC抛异常时失败")
void testFail_RpcException() {
when(rpcClient.sendTx("tx-003")).thenThrow(new RuntimeException());
assertFalse(multisigService.executeTx("tx-003"));
}
}
}
- 静态方法 Mock + JUnit 5(区块链高频场景)
针对智能合约后端的静态工具类(如签名工具),结合 Mockito 静态 Mock 和 JUnit 5:
点击查看代码
import org.mockito.MockedStatic;
@ExtendWith(MockitoExtension.class)
class StaticMockIntegrationTest {
@Mock
private RpcClient rpcClient;
@InjectMocks
private MultisigService multisigService;
@Test
@DisplayName("Mock静态签名工具 + 验证交易执行")
void testStaticMock() {
// --- Mockito:Mock静态工具类(try-with-resources自动关闭) ---
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
// 定制静态方法返回值
mockedStatic.when(() -> SignatureUtils.sign("tx-001"))
.thenReturn("mock-signature-001");
// 定制RPC行为
when(rpcClient.sendTx("tx-001")).thenReturn(true);
// --- JUnit 5:执行被测方法(内部调用静态签名+RPC) ---
boolean result = multisigService.executeTxWithSign("tx-001");
// --- 断言 + 验证 ---
assertTrue(result);
// 验证静态方法被调用
mockedStatic.verify(() -> SignatureUtils.sign("tx-001"), times(1));
// 验证RPC被调用
verify(rpcClient, times(1)).sendTx("tx-001");
}
}
}
// 静态签名工具类(区块链场景高频)
class SignatureUtils {
public static String sign(String txId) {
// 真实逻辑:调用MPC签名服务
return "real-sign-" + txId;
}
}
// 扩展多签服务:增加签名逻辑
class MultisigService {
private RpcClient rpcClient;
public MultisigService(RpcClient rpcClient) {
this.rpcClient = rpcClient;
}
public boolean executeTxWithSign(String txId) {
// 调用静态签名工具
String signature = SignatureUtils.sign(txId);
if (signature == null) return false;
// 调用RPC发送交易
return rpcClient.sendTx(txId);
}
// 原有executeTx方法...
}
四、完整实战示例(智能合约后端场景)
以下是覆盖 “多签服务 + RPC 客户端 + 静态签名 + 异常场景” 的完整测试代码,可直接复制运行:
点击查看代码
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
// 核心整合注解
@ExtendWith(MockitoExtension.class)
@DisplayName("智能合约后端:多签服务完整测试(JUnit 5 + Mockito)")
// JUnit 5:测试实例仅创建一次(@BeforeAll可非静态)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CompleteMultisigTest {
// ========== Mockito:创建Mock对象 ==========
@Mock
private RpcClient rpcClient; // 模拟链上RPC客户端
// ========== Mockito:注入被测对象 ==========
@InjectMocks
private MultisigService multisigService; // 被测服务
// ========== JUnit 5:全局初始化 ==========
@BeforeAll
void initGlobal() {
System.out.println("===== 全局初始化:仅执行一次 =====");
}
// ========== JUnit 5:每个测试方法前置初始化 ==========
@BeforeEach
void setUp() {
System.out.println("----- 测试方法前置初始化 -----");
}
// ========== 场景1:基础成功测试 ==========
@Test
@DisplayName("场景1:交易签名正常 + RPC返回成功 → 执行成功")
void testSuccess() {
// Mockito:定制静态签名+RPC行为
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
mockedStatic.when(() -> SignatureUtils.sign("tx-001")).thenReturn("mock-sign-001");
when(rpcClient.sendTx("tx-001", "mock-sign-001")).thenReturn(true);
// 执行被测方法
boolean result = multisigService.executeMultisigTx("tx-001");
// JUnit 5:断言结果
assertTrue(result);
// Mockito:验证交互
mockedStatic.verify(() -> SignatureUtils.sign("tx-001"), times(1));
verify(rpcClient, times(1)).sendTx("tx-001", "mock-sign-001");
}
}
// ========== 场景2:参数化测试多失败场景 ==========
@ParameterizedTest
@CsvSource({
"tx-002, null, false", // 签名返回null → 失败
"tx-003, mock-sign-003, false", // RPC返回false → 失败
"tx-004, mock-sign-004, exception" // RPC抛异常 → 失败
})
@DisplayName("场景2:参数化测试交易执行失败")
void testFailWithParams(String txId, String signResult, String rpcBehavior) {
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
// Mockito:定制静态签名行为
mockedStatic.when(() -> SignatureUtils.sign(txId)).thenReturn(signResult);
// Mockito:定制RPC行为
if ("false".equals(rpcBehavior)) {
when(rpcClient.sendTx(txId, signResult)).thenReturn(false);
} else if ("exception".equals(rpcBehavior)) {
when(rpcClient.sendTx(txId, signResult)).thenThrow(new RuntimeException("RPC超时"));
}
// 执行被测方法
boolean result = multisigService.executeMultisigTx(txId);
// JUnit 5:断言结果(所有场景都应失败)
assertFalse(result);
// Mockito:验证交互
mockedStatic.verify(() -> SignatureUtils.sign(txId), times(1));
if (signResult != null) { // 签名非空时才会调用RPC
verify(rpcClient, times(1)).sendTx(txId, signResult);
} else {
verify(rpcClient, never()).sendTx(anyString(), anyString());
}
}
}
// ========== 场景3:嵌套测试 - 边界场景 ==========
@Nested
@DisplayName("场景3:嵌套测试 - 边界场景")
class BoundaryScenarios {
@Test
@DisplayName("边界1:空交易ID → 直接失败")
void testBoundary_EmptyTxId() {
boolean result = multisigService.executeMultisigTx("");
assertFalse(result);
verifyNoInteractions(rpcClient); // 未调用RPC
}
@Test
@DisplayName("边界2:签名为空字符串 → 失败")
void testBoundary_EmptySign() {
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
mockedStatic.when(() -> SignatureUtils.sign("tx-005")).thenReturn("");
boolean result = multisigService.executeMultisigTx("tx-005");
assertFalse(result);
verify(rpcClient, never()).sendTx(anyString(), anyString());
}
}
}
// ========== JUnit 5:测试方法后置清理 ==========
@AfterEach
void tearDown() {
reset(rpcClient); // Mockito:重置Mock对象
System.out.println("----- 测试方法后置清理 -----");
}
// ========== JUnit 5:全局清理 ==========
@AfterAll
void cleanGlobal() {
System.out.println("===== 全局清理:仅执行一次 =====");
}
// ========== 依赖类定义 ==========
// RPC客户端接口
interface RpcClient {
boolean sendTx(String txId, String signature);
}
// 静态签名工具类
class SignatureUtils {
public static String sign(String txId) {
return "real-sign-" + txId;
}
}
// 被测多签服务类
class MultisigService {
private RpcClient rpcClient;
public MultisigService(RpcClient rpcClient) {
this.rpcClient = rpcClient;
}
public boolean executeMultisigTx(String txId) {
// 边界校验:空交易ID直接失败
if (txId == null || txId.isEmpty()) {
return false;
}
// 1. 调用静态签名工具
String signature = SignatureUtils.sign(txId);
if (signature == null || signature.isEmpty()) {
return false;
}
// 2. 调用RPC发送交易
try {
return rpcClient.sendTx(txId, signature);
} catch (Exception e) {
return false;
}
}
}
}
学习技术不是用来写HelloWorld和Demo的,而是要用来解决线上系统的真实问题的.

浙公网安备 33010602011771号