惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
Recorded Future
Recorded Future
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Microsoft Azure Blog
Microsoft Azure Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
M
MIT News - Artificial intelligence
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
N
News | PayPal Newsroom
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Help Net Security
Help Net Security
博客园 - Franky
SecWiki News
SecWiki News
Recent Announcements
Recent Announcements
T
Troy Hunt's Blog
The Register - Security
The Register - Security
The Last Watchdog
The Last Watchdog
Webroot Blog
Webroot Blog
S
Security Affairs
博客园 - 司徒正美
S
Schneier on Security
I
InfoQ
博客园_首页
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Threat Research - Cisco Blogs
Forbes - Security
Forbes - Security
腾讯CDC
N
Netflix TechBlog - Medium
N
News and Events Feed by Topic
Cloudbric
Cloudbric
T
The Exploit Database - CXSecurity.com
P
Proofpoint News Feed
A
About on SuperTechFans
Engineering at Meta
Engineering at Meta
Recent Commits to openclaw:main
Recent Commits to openclaw:main
B
Blog
V
Vulnerabilities – Threatpost
C
Check Point Blog
Google DeepMind News
Google DeepMind News
Google Online Security Blog
Google Online Security Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cisco Blogs
Schneier on Security
Schneier on Security
O
OpenAI News
K
Kaspersky official blog

博客园 - cac2020

QA之三 - 变异测试 -- PITest QA之二 - 单元测试-- JaCoCo QA之二 - 单元测试--Mockito QA之二 - 单元测试--JUnit5 QA之二 -- 测试用例 QA之一 -- 常见测试类型和测试环境 密码学之三 —— MPC(TSS)、多签、时间锁 密码学之二 —— GPG、SSH、KMS 密码学之一 —— 三类算法 记录一次在线word转PDF的问题处理过程 分析Java堆Dump Java GC日志分析 Java线程Dump分析(TDA-Thread Dump Analyzer) Java可视化故障处理工具-VisualVM Java基础故障处理工具 记录Arthas在一次性能调优过程中实践 中小型系统必要可行的性能测试实践--ArtHas调优实战 中小型系统必要可行的性能测试实践--jmeter落地实践 中小型系统必要可行的性能测试实践--性能测试理论基础
QA之二 - 单元测试-- JUnit5 + Mockito
cac2020 · 2026-02-25 · via 博客园 - cac2020

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

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

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;
            }
        }
    }
}

学习技术不是用来写HelloWorld和Demo的,而是要用来解决线上系统的真实问题的.