QA之二 - 单元测试-- JUnit5 + Mockito

单元测试是针对软件中最小的可测试单元(如单个函数、方法、合约函数)进行的独立测试,目的是验证这个单元的逻辑是否完全符合预期,且仅关注该单元本身,不依赖外部模块 / 服务。

通俗解释:把复杂的系统拆成最小的 “零件” 逐一测试:比如测试一辆车:单元测试不测试 “整车行驶”,只测试 “单个刹车卡钳”“单个发动机气缸” 是否正常工作。

Java开发中最常用的单元测试组合:JUnit5 + Mockito 。


一、JUnit 5 + Mockito
JUnit 5 与 Mockito 结合使用 —— 这是 Java 单元测试的黄金组合,核心是用 Mockito 隔离外部依赖,用 JUnit 5 管理测试生命周期和断言。

二、整合步骤

  1. 第一步:引入依赖(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>

  1. 核心整合注解
    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;
        }
    }
}

三、进阶用法

  1. 参数化测试 + 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);
    }
}
  1. 嵌套测试 + 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"));
        }
    }
}
  1. 静态方法 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;
            }
        }
    }
}
posted @ 2026-02-25 23:52  cac2020  阅读(13)  评论(0)    收藏  举报