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>
(2)静态 Mock 示例(模拟签名工具类)
点击查看代码
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);
        }
    }
}
posted @ 2026-02-25 20:42  cac2020  阅读(13)  评论(0)    收藏  举报