QA之二 - 单元测试--Mockito
一、是什么
Mockito 是 Java 生态最主流的模拟框架(Mock Framework),能创建 “模拟对象(Mock Object)” 替代真实的外部依赖(如 RPC 客户端、数据库连接、第三方 API),让单元测试完全隔离外部系统,只关注被测代码的逻辑。
核心概念:
- Mock 对象:模拟外部依赖的 “假对象”,可定制返回值 / 验证交互,比如模拟 RpcClient 类,替代真实的链上 RPC 连接;
- Stub(存根):仅定制 Mock 对象的返回值(给数据),如让 Mock 的 getBlockNumber() 固定返回 123456;
- 行为验证:验证被测代码是否调用了 Mock 对象的指定方法,比如验证多签服务是否调用了 RpcClient.sendTx() 方法;
二、怎么用
1、引入Maven依赖,配合JUnit5
点击查看代码
<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>
<!-- 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>
2、Mockito 基础用法
Mockito 的核心操作分为 3 步:创建 Mock 对象 → 定制 Mock 行为(Stub) → 执行测试 & 验证交互,以下是基础用法:
2.1. 核心注解(简化 Mock 对象创建)
Mockito 提供注解替代手动创建 Mock 对象,结合 JUnit 5 需在测试类上加 @ExtendWith(MockitoExtension.class):
- @Mock:创建 Mock 对象;
- @InjectMocks:自动将 Mock 对象注入到被测对象中(依赖注入);
- @Spy:包装真实对象(部分模拟:真实方法 + Mock 方法);
- @Captor:捕获方法调用的参数(验证参数是否正确)
基础示例(注解使用)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
// 核心注解:启用 Mockito 对 JUnit 5 的支持
@ExtendWith(MockitoExtension.class)
class MultisigServiceTest {
// 1. 创建 Mock 对象(模拟链上RPC客户端)
@Mock
private RpcClient rpcClient;
// 2. 将 Mock 对象注入到被测对象中(MultisigService 依赖 RpcClient)
@InjectMocks
private MultisigService multisigService; // 被测对象
// 测试方法...
}
2.2. 定制 Mock 行为(Stub:给 Mock 对象 “设定返回值”)
用 when(...).thenReturn(...) 定制 Mock 方法的返回值,或 when(...).thenThrow(...) 模拟异常场景:
(1)基础返回值定制
点击查看代码
import static org.mockito.Mockito.*;
@Test
@DisplayName("模拟RPC客户端返回区块高度")
void testMockRpcClient() {
// 定制行为:当调用 rpcClient.getBlockNumber() 时,返回 123456
when(rpcClient.getBlockNumber()).thenReturn(123456L);
// 调用被测方法(内部会调用 rpcClient.getBlockNumber())
long blockNumber = multisigService.getCurrentBlockNumber();
// 断言结果(验证被测方法逻辑)
assertEquals(123456L, blockNumber);
}
(2)模拟异常场景(如 RPC 调用失败)
点击查看代码
@Test
@DisplayName("模拟RPC调用失败抛出异常")
void testRpcClientException() {
// 定制行为:调用 rpcClient.sendTx("0x123") 时抛出 RuntimeException
when(rpcClient.sendTx("0x123")).thenThrow(new RuntimeException("RPC连接超时"));
// 断言被测方法会捕获异常并返回false
boolean result = multisigService.executeTransaction("0x123");
assertFalse(result);
}
(3)多次调用返回不同值(链式调用)
点击查看代码
@Test
@DisplayName("模拟多次调用RPC返回不同值")
void testMultipleCalls() {
// 第一次调用返回 123456,第二次返回 123457
when(rpcClient.getBlockNumber())
.thenReturn(123456L)
.thenReturn(123457L);
// 验证多次调用结果
assertEquals(123456L, rpcClient.getBlockNumber());
assertEquals(123457L, rpcClient.getBlockNumber());
}
(4)参数匹配(模糊匹配参数)
当不知道方法调用的具体参数,或需要匹配一类参数时,用 Mockito 的参数匹配器:
- any():匹配任意类型参数:when(rpcClient.sendTx(any())).thenReturn(true)
- anyString():匹配任意字符串:when(rpcClient.getTx(anyString())).thenReturn(tx)
- eq("0x123"):匹配指定值:when(rpcClient.sendTx(eq("0x123"))).thenReturn(true)
- isNull()/notNull():匹配空 / 非空参数:when(rpcClient.getTx(isNull())).thenThrow(IllegalArgumentException.class)
点击查看代码
@Test
@DisplayName("参数匹配器:模拟任意交易ID返回成功")
void testArgumentMatcher() {
// 定制行为:sendTx 接收任意字符串参数时,都返回 true
when(rpcClient.sendTx(anyString())).thenReturn(true);
// 验证不同参数都返回 true
assertTrue(rpcClient.sendTx("0x123"));
assertTrue(rpcClient.sendTx("0x456"));
}
2.3. 行为验证(验证 Mock 对象是否被正确调用)
除了验证返回值,还能验证 “被测代码是否调用了 Mock 对象的指定方法”“调用次数是否正确”:
核心验证语法
// 基础验证:验证方法被调用过一次
verify(rpcClient).sendTx("0x123");
// 验证调用次数
verify(rpcClient, times(2)).sendTx(anyString()); // 调用2次
verify(rpcClient, never()).sendTx("0x789"); // 从未调用
verify(rpcClient, atLeastOnce()).getBlockNumber(); // 至少调用1次
verify(rpcClient, atMost(3)).getBlockNumber(); // 最多调用3次
// 验证方法未被调用
verifyNoInteractions(rpcClient); // 完全未调用
verifyNoMoreInteractions(rpcClient); // 除了已验证的调用,无其他调用
2.4. @Spy 部分模拟(真实对象 + Mock 方法)
@Spy 用于包装真实对象,既可以调用真实方法,也可以 Mock 部分方法(适合 “大部分逻辑用真实,少数方法模拟” 的场景):
点击查看代码
@Test
@DisplayName("@Spy 部分模拟时间锁服务")
void testSpy() {
// 创建真实对象的 Spy 包装
TimelockService timelockService = new TimelockService();
TimelockService spyService = spy(timelockService);
// 真实方法:调用 calculateDelay() 执行真实逻辑
assertEquals(3600, spyService.calculateDelay());
// Mock 方法:定制 isTimeLockPassed() 返回 false
doReturn(false).when(spyService).isTimeLockPassed("0x123");
assertFalse(spyService.isTimeLockPassed("0x123"));
}
关键注意
- @Spy 和 @Mock 的区别:@Mock 是完全模拟(所有方法默认返回默认值),@Spy 是部分模拟(默认调用真实方法);
- 定制 @Spy 方法的返回值,需用 doReturn(...) 而非 when(...)(避免触发真实方法)。
2.5. @Captor 参数捕获(验证方法调用的参数)
用 @Captor 捕获 Mock 方法调用时传入的参数,验证参数是否符合预期(如 “多签服务调用 RPC 时,传入的交易 ID 是否正确”):
点击查看代码
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@ExtendWith(MockitoExtension.class)
class MultisigServiceTest {
@Mock
private RpcClient rpcClient;
@InjectMocks
private MultisigService multisigService;
// 定义参数捕获器(捕获 String 类型的参数)
@Captor
private ArgumentCaptor<String> txIdCaptor;
@Test
@DisplayName("捕获参数:验证调用RPC的交易ID是否正确")
void testArgumentCaptor() {
// 1. 执行被测方法(内部会调用 rpcClient.sendTx("0x123456"))
multisigService.executeTransaction("0x123456");
// 2. 捕获 sendTx 方法的参数
verify(rpcClient).sendTx(txIdCaptor.capture());
// 3. 验证捕获的参数
String capturedTxId = txIdCaptor.getValue();
assertEquals("0x123456", capturedTxId);
}
}
3.Mockito进阶用法
3.1 Mock 静态方法(Mockito 3.4+ 支持)
区块链开发中常遇到静态工具类(如 SignatureUtils.sign()),Mockito 支持 Mock 静态方法(需额外依赖):
(1)引入静态 Mock 依赖
点击查看代码
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
点击查看代码
import static org.mockito.Mockito.mockStatic;
@Test
@DisplayName("Mock 静态签名工具类")
void testStaticMethod() {
// 1. Mock 静态类(try-with-resources 自动关闭)
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
// 2. 定制静态方法返回值
mockedStatic.when(() -> SignatureUtils.sign("txData123"))
.thenReturn("0xabcdef123456");
// 3. 执行被测方法
String signature = multisigService.signTransaction("txData123");
// 4. 验证结果和静态方法调用
assertEquals("0xabcdef123456", signature);
mockedStatic.verify(() -> SignatureUtils.sign("txData123"), times(1));
}
}
3.2. Mock 私有方法(间接方式)
Mockito 不直接支持 Mock 私有方法,推荐通过 “重构代码(将私有方法抽为公共方法)” 或用 Spy 模拟:
点击查看代码
// 被测类:私有方法 calculateFee()
public class MultisigService {
private RpcClient rpcClient;
private long calculateFee(String txId) {
return rpcClient.getGasPrice() * 21000;
}
public long getTransactionFee(String txId) {
return calculateFee(txId);
}
}
// 测试类:用 Spy 模拟私有方法依赖的 rpcClient
@Test
void testPrivateMethod() {
// 1. Mock rpcClient(私有方法依赖的对象)
when(rpcClient.getGasPrice()).thenReturn(100L);
// 2. 执行被测方法(间接调用私有方法)
long fee = multisigService.getTransactionFee("0x123");
// 3. 断言结果(私有方法逻辑:100 * 21000 = 2100000)
assertEquals(2100000L, fee);
}
3.3. 重置 Mock 对象(复用 Mock)
若需在单个测试方法中多次修改 Mock 行为,可重置 Mock 对象:
点击查看代码
@Test
void testResetMock() {
// 第一次定制行为
when(rpcClient.getBlockNumber()).thenReturn(123456L);
assertEquals(123456L, rpcClient.getBlockNumber());
// 重置 Mock
reset(rpcClient);
// 第二次定制行为
when(rpcClient.getBlockNumber()).thenReturn(789012L);
assertEquals(789012L, rpcClient.getBlockNumber());
}
4.Mockito 实战示例(区块链 / 智能合约后端)
以下是完整的实战示例,模拟 “多签服务调用 RPC 客户端 + 签名工具类” 的单元测试,完全隔离外部依赖:
(1)被测类(多签服务,依赖 RPC 客户端和签名工具)
点击查看代码
// MultisigService.java(业务类)
public class MultisigService {
private RpcClient rpcClient; // 外部依赖:链上RPC客户端
// 构造注入(方便Mockito注入Mock对象)
public MultisigService(RpcClient rpcClient) {
this.rpcClient = rpcClient;
}
// 核心方法:执行多签交易(依赖RPC和签名)
public boolean executeMultisigTx(String txId, String txData) {
try {
// 1. 签名交易
String signature = SignatureUtils.sign(txData);
if (signature == null || signature.isEmpty()) {
return false;
}
// 2. 调用RPC发送交易
boolean sendResult = rpcClient.sendTx(txId, signature);
if (!sendResult) {
return false;
}
// 3. 获取区块高度(验证交易上链)
long blockNumber = rpcClient.getBlockNumber();
return blockNumber > 0;
} catch (Exception e) {
return false;
}
}
}
// 依赖类1:RPC客户端(外部服务)
public interface RpcClient {
boolean sendTx(String txId, String signature);
long getBlockNumber();
}
// 依赖类2:静态签名工具类
public class SignatureUtils {
public static String sign(String data) {
// 真实逻辑:调用MPC签名服务(外部依赖)
return "real-signature-" + data;
}
}
(2) Mockito 测试类(完全隔离外部依赖)
点击查看代码
// MultisigServiceMockitoTest.java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.MockedStatic;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("多签服务 Mockito 单元测试")
class MultisigServiceMockitoTest {
// 1. Mock 外部依赖:RPC客户端
@Mock
private RpcClient rpcClient;
// 2. 将 Mock 对象注入被测对象
@InjectMocks
private MultisigService multisigService;
// 3. 参数捕获器:捕获 sendTx 的 txId 参数
@Captor
private ArgumentCaptor<String> txIdCaptor;
@Captor
private ArgumentCaptor<String> signatureCaptor;
@Test
@DisplayName("测试执行多签交易成功(所有依赖正常)")
void testExecuteMultisigTxSuccess() {
// ========== 步骤1:定制所有 Mock 行为 ==========
// 1. Mock 静态签名工具类
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
// 定制静态方法返回模拟签名
mockedStatic.when(() -> SignatureUtils.sign("test-data"))
.thenReturn("mock-signature-test-data");
// 2. Mock RPC客户端行为
when(rpcClient.sendTx(anyString(), anyString())).thenReturn(true);
when(rpcClient.getBlockNumber()).thenReturn(123456L);
// ========== 步骤2:执行被测方法 ==========
boolean result = multisigService.executeMultisigTx("tx-001", "test-data");
// ========== 步骤3:验证结果和交互 ==========
// 1. 断言返回值正确
assertTrue(result);
// 2. 验证静态签名方法被调用
mockedStatic.verify(() -> SignatureUtils.sign("test-data"), times(1));
// 3. 验证RPC.sendTx被调用,且参数正确
verify(rpcClient, times(1)).sendTx(txIdCaptor.capture(), signatureCaptor.capture());
assertEquals("tx-001", txIdCaptor.getValue());
assertEquals("mock-signature-test-data", signatureCaptor.getValue());
// 4. 验证RPC.getBlockNumber被调用
verify(rpcClient, times(1)).getBlockNumber();
}
}
@Test
@DisplayName("测试执行多签交易失败(RPC发送交易失败)")
void testExecuteMultisigTxFail_RpcSendFailed() {
// ========== 步骤1:定制 Mock 行为 ==========
// 1. Mock 静态签名返回正常
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
mockedStatic.when(() -> SignatureUtils.sign("test-data")).thenReturn("mock-signature");
// 2. Mock RPC.sendTx 返回失败
when(rpcClient.sendTx(anyString(), anyString())).thenReturn(false);
// ========== 步骤2:执行被测方法 ==========
boolean result = multisigService.executeMultisigTx("tx-002", "test-data");
// ========== 步骤3:验证结果和交互 ==========
// 1. 断言返回失败
assertFalse(result);
// 2. 验证RPC.sendTx被调用,但getBlockNumber未被调用
verify(rpcClient, times(1)).sendTx(anyString(), anyString());
verify(rpcClient, never()).getBlockNumber();
}
}
@Test
@DisplayName("测试执行多签交易失败(签名抛出异常)")
void testExecuteMultisigTxFail_SignException() {
// ========== 步骤1:定制 Mock 行为 ==========
// 1. Mock 静态签名抛出异常
try (MockedStatic<SignatureUtils> mockedStatic = mockStatic(SignatureUtils.class)) {
mockedStatic.when(() -> SignatureUtils.sign("test-data"))
.thenThrow(new RuntimeException("签名服务超时"));
// ========== 步骤2:执行被测方法 ==========
boolean result = multisigService.executeMultisigTx("tx-003", "test-data");
// ========== 步骤3:验证结果和交互 ==========
// 1. 断言返回失败
assertFalse(result);
// 2. 验证RPC方法未被调用
verifyNoInteractions(rpcClient);
}
}
}

浙公网安备 33010602011771号