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

推荐订阅源

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之二 - 单元测试-- JUnit5 + Mockito QA之二 - 单元测试--Mockito 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
cac2020 · 2026-02-24 · via 博客园 - cac2020

JUnit5
一、是什么?
JUnit 5 是 Java 生态最新的单元测试框架(替代 JUnit 4),全称「JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage」,专为 Java 8 + 设计,支持 Lambda 表达式、流式 API,适配现代 Java 开发。

三大模块:

  • JUnit Platform:测试运行的基础平台(启动测试、对接 IDE / 构建工具如 Maven/Gradle);
  • JUnit Jupiter:核心模块:定义 JUnit 5 的测试注解、断言、测试接口等(写测试用例的核心);
  • JUnit Vintage:兼容 JUnit 4/3 的测试用例(过渡用);

二、核心注解
JUnit 5 的注解全部在org.junit.jupiter.api包下,以下是开发中最常用的:

  • @Test:标记方法为测试方法(无需public,可直接用void/ 有返回值);
  • @DisplayName:自定义测试类 / 方法的显示名称(易读,如中文描述);
  • @BeforeEach:每个测试方法执行前运行(初始化测试数据 / 对象),如初始化多签服务实例、模拟数据库连接;
  • @AfterEach:每个测试方法执行后运行(清理资源),如关闭模拟连接、重置测试状态;
  • @BeforeAll:所有测试方法执行前运行一次(静态方法),如加载配置文件、初始化全局测试资源;
  • @AfterAll:所有测试方法执行后运行一次(静态方法),如释放全局资源、关闭连接池;
  • @Disabled:禁用某个测试方法 / 类(临时跳过),如@Disabled("暂不测试,待修复bug");
  • @Nested:嵌套测试(按业务逻辑分组测试,如 “多签门限测试” 嵌套在 “多签服务测试” 下),结构化管理测试用例,更清晰;
  • @Tag:给测试打标签(按标签执行测试,如@Tag("core")/@Tag("unit"),区分 “核心逻辑测试” 和 “非核心逻辑测试”;

关键注意(和 JUnit4 的区别)

  • JUnit5:@BeforeEach/@AfterEach/@BeforeAll/@AfterAll(去掉了 JUnit 4 的@Before/@After);
  • @Test 无需public修饰,JUnit 5 更简洁;
  • 测试生命周期
    • PER_METHOD:默认模式,每个测试方法创建一个实例;

      • 生命周期流程:@BeforeAll(全局初始化) --> 创建测试实例1 --> @BeforeEach(方法1前置) --> @Test 方法1 --> @AfterEach(方法1后置) --> 创建测试实例2"] --> @BeforeEach(方法2前置) --> @Test 方法2 --> @AfterEach(方法2后置) --> @AfterAll(全局清理)
    • PER_CLASS:需给测试类加@TestInstance(TestInstance.Lifecycle.PER_CLASS),测试实例仅创建一次;

      • @BeforeAll/@AfterAll可非静态;
      • 生命周期流程:1. 初始化测试类(仅1次) --> 2. 执行@BeforeAll(非静态,仅1次) --> 3. 执行@BeforeEach(测试方法1前置) --> 4. 执行@Test 方法1 --> 5. 执行@AfterEach(测试方法1后置) --> 6. 执行@BeforeEach(测试方法2前置) --> 7. 执行@Test 方法2 --> 8. 执行@AfterEach(测试方法2后置) --> 9. 执行@AfterAll(非静态,仅1次) --> 10. 销毁测试实例(仅1次)

三、关键断言
JUnit 5 的断言在org.junit.jupiter.api.Assertions类中,支持 Lambda 表达式、异常断言、超时断言,核心用法如下:
(1)基础断言(最常用)

点击查看代码
import static org.junit.jupiter.api.Assertions.*;

@Test
@DisplayName("测试多签门限校验-基础断言")
void testSignatureThreshold() {
    // 1. 相等断言(核心)
    int actual = multisigService.checkSignatureCount(2); // 实际结果
    int expected = 2; // 预期结果
    assertEquals(expected, actual, "签名数校验结果不匹配"); // 支持自定义错误信息

    // 2. 布尔断言
    boolean isValid = multisigService.isValidThreshold(2);
    assertTrue(isValid, "门限=2时应返回true");
    assertFalse(multisigService.isValidThreshold(1), "门限=1时应返回false");

    // 3. 空值断言
    String txId = multisigService.getTxId(1);
    assertNotNull(txId, "交易ID不应为空");
    assertNull(multisigService.getTxId(-1), "无效ID应返回null");

    // 4. 相同引用断言(判断对象是否是同一个)
    assertSame(multisigService, multisigService, "对象引用应相同");
}

(2)异常断言(测试异常场景,如多签签名数不足)

点击查看代码
@Test
@DisplayName("测试签名数不足时抛出异常")
void testSignatureInsufficient() {
    // 断言执行lambda表达式时会抛出指定异常
    assertThrows(IllegalArgumentException.class, 
        () -> multisigService.executeTx(1), // 执行会抛异常的方法
        "签名数不足时应抛出IllegalArgumentException");

    // 进阶:获取异常对象,验证异常信息
    IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, 
        () -> multisigService.executeTx(1));
    assertEquals("签名数不足(需≥2)", exception.getMessage());
}

(3)超时断言(测试方法执行超时,如时间锁计算)

点击查看代码
@Test
@DisplayName("测试时间锁到期判断不超时")
void testTimelockCheckTimeout() {
    // 断言方法执行时间≤100ms
    assertTimeout(Duration.ofMillis(100), 
        () -> timelockService.isTimeLockPassed(1),
        "时间锁校验应在100ms内完成");
}

(4)组合断言(批量验证,失败时全部展示)

点击查看代码
@Test
@DisplayName("组合断言验证多签交易状态")
void testTxStatus() {
    // 所有断言都执行,失败的会全部列出(而非第一个失败就停止)
    assertAll("交易状态校验",
        () -> assertEquals(2, multisigService.getSignatureCount(1)),
        () -> assertTrue(multisigService.isTxPending(1)),
        () -> assertFalse(multisigService.isTxExecuted(1))
    );
}

四、高阶特性

  1. 参数化测试(批量测试多场景)
    核心解决:重复写 “输入不同参数、验证不同结果” 的测试用例(如多签门限 = 1/2/3),用@ParameterizedTest实现,需依赖junit-jupiter-params包。
    (1)基础参数化(@ValueSource)
点击查看代码
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

// 测试多签门限的多个场景:1(无效)、2(有效)、3(有效)
@ParameterizedTest
@ValueSource(ints = {1, 2, 3}) // 传入int类型参数列表
@DisplayName("参数化测试多签门限有效性")
void testThresholdValid(int threshold) {
    boolean result = multisigService.isValidThreshold(threshold);
    // 断言:threshold=1时false,2/3时true
    if (threshold == 1) {
        assertFalse(result);
    } else {
        assertTrue(result);
    }
}

(2)多参数测试(@CsvSource)

点击查看代码
@ParameterizedTest
@CsvSource({
    "0, false", // 签名数=0 → 无效
    "1, false", // 签名数=1 → 无效
    "2, true",  // 签名数=2 → 有效
    "3, true"   // 签名数=3 → 有效
})
@DisplayName("多参数测试签名数有效性")
void testSignatureCountValid(int count, boolean expected) {
    boolean actual = multisigService.isValidSignatureCount(count);
    assertEquals(expected, actual, "签名数=" + count + "时结果错误");
}

(3)方法来源参数(@MethodSource)

点击查看代码
// 1. 定义参数生成方法(静态,返回Stream)
static Stream<Arguments> signatureData() {
    return Stream.of(
        Arguments.of(0, false),
        Arguments.of(2, true),
        Arguments.of(5, true)
    );
}

// 2. 参数化测试引用该方法
@ParameterizedTest
@MethodSource("signatureData")
@DisplayName("方法来源测试签名数")
void testSignatureFromMethod(int count, boolean expected) {
    assertEquals(expected, multisigService.isValidSignatureCount(count));
}
  1. 嵌套测试(@Nested)
    按业务逻辑分组测试,让测试用例更结构化(如 “多签服务” 下分 “门限测试”“执行测试”):
点击查看代码
@DisplayName("多签服务单元测试")
class MultisigServiceTest {
    private MultisigService multisigService;

    @BeforeEach
    void init() {
        multisigService = new MultisigService(2); // 初始化门限=2
    }

    // 嵌套测试:门限相关测试
    @Nested
    @DisplayName("门限校验模块")
    class ThresholdTest {
        @Test
        @DisplayName("门限=2时校验通过")
        void testThreshold2() {
            assertTrue(multisigService.isValidThreshold(2));
        }

        @Test
        @DisplayName("门限=1时校验失败")
        void testThreshold1() {
            assertFalse(multisigService.isValidThreshold(1));
        }
    }

    // 嵌套测试:执行交易模块
    @Nested
    @DisplayName("执行交易模块")
    class ExecuteTxTest {
        @Test
        @DisplayName("签名数=2时执行成功")
        void testExecuteSuccess() {
            assertTrue(multisigService.executeTx(1));
        }

        @Test
        @DisplayName("签名数=1时执行失败")
        void testExecuteFail() {
            assertThrows(IllegalArgumentException.class, () -> multisigService.executeTx(2));
        }
    }
}

五、实战

  1. 第一步:引入依赖(Maven)
点击查看代码
<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>
    </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. 被测类(多签服务)
点击查看代码
package com.example;

public class MultisigService {
    // 多签门限(默认2)
    private int threshold;

    public MultisigService(int threshold) {
        this.threshold = threshold;
    }

    public int checkSignatureCount(){
        return threshold;
    }

    // 校验签名数是否达标
    public boolean isValidThreshold(int count) {
        return count >= threshold;
    }

    public boolean isTxPending(int num){
        return num == 1;
    }

    public boolean isTxExecuted(int num){
        return num != 1;
    }

    // 执行交易(签名数不足抛异常)
    public boolean executeTx(int txId) {
        int signatureCount = getSignatureCount(txId);
        if (!isValidThreshold(signatureCount)) {
            throw new IllegalArgumentException("签名数不足(需≥" + threshold + ")");
        }
        // 模拟执行交易
        return true;
    }

    public void isTimeLockPassed(long time){
        try {
            Thread.sleep( time );
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public String getTxId(int nounce){
        return "txId-" + nounce;
    }

    // 模拟获取签名数(测试用)
    private int getSignatureCount(int txId) {
        // txId=1返回2,其他返回1
        return txId == 1 ? 2 : 1;
    }
}
  1. JUnit 5 测试类
点击查看代码
// MultisigServiceTest.java(测试类)
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

// 全局显示名称
@DisplayName("多签服务单元测试(JUnit 5)")
// 测试实例仅创建一次(@BeforeAll可非静态)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MultisigServiceTest {
    private MultisigService multisigService;

    // 全局初始化(所有测试前执行一次)
    @BeforeAll
    @DisplayName("初始化多签服务")
    void initGlobal() {
        System.out.println("===== 全局初始化 =====");
    }

    // 每个测试方法前初始化
    @BeforeEach
    @DisplayName("初始化测试数据")
    void init() {
        multisigService = new MultisigService(2); // 门限=2
        System.out.println("----- 测试方法初始化 -----");
    }

    // 基础测试:签名数=2时有效
    @Test
    @DisplayName("测试签名数=2时校验通过")
    void testValidSignatureCount() {
        boolean result = multisigService.isValidSignatureCount(2);
        assertTrue(result, "签名数=2应返回true");
    }

    // 异常测试:签名数不足抛异常
    @Test
    @DisplayName("测试签名数=1时执行交易抛异常")
    void testExecuteTxWithInsufficientSignature() {
        // 断言抛异常,且异常信息正确
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> multisigService.executeTx(2), // txId=2 → 签名数=1
                "签名数不足时应抛IllegalArgumentException");
        assertEquals("签名数不足(需≥2)", exception.getMessage());
    }

    // 参数化测试:多场景校验签名数
    @ParameterizedTest
    @CsvSource({"0,false", "1,false", "2,true", "3,true"})
    @DisplayName("参数化测试-签名数有效性")
    void testSignatureCountParam(int count, boolean expected) {
        boolean actual = multisigService.isValidSignatureCount(count);
        assertEquals(expected, actual, "签名数=" + count + "时结果错误");
    }

    // 嵌套测试:执行交易模块
    @Nested
    @DisplayName("执行交易子模块测试")
    class ExecuteTxSubTest {
        @Test
        @DisplayName("txId=1时执行成功(签名数=2)")
        void testExecuteSuccess() {
            assertTrue(multisigService.executeTx(1));
        }

        @Test
        @DisplayName("txId=2时执行失败(签名数=1)")
        void testExecuteFail() {
            assertThrows(IllegalArgumentException.class, () -> multisigService.executeTx(2));
        }
    }

    // 禁用测试(临时跳过)
    @Test
    @Disabled("暂不测试,待优化逻辑")
    @DisplayName("禁用测试-时间锁集成")
    void testTimelockIntegration() {
        // 待实现
    }

    // 每个测试方法后清理
    @AfterEach
    @DisplayName("清理测试数据")
    void clean() {
        System.out.println("----- 测试方法清理 -----");
    }

    // 全局清理(所有测试后执行一次)
    @AfterAll
    @DisplayName("全局清理")
    void cleanGlobal() {
        System.out.println("===== 全局清理 =====");
    }
}
  1. 运行测试(IDEA/Maven)
    IDEA:直接右键测试类→Run MultisigServiceTest,可看到带中文的测试名称,清晰易读;
    Maven:执行命令 mvn test -Dtest=测试类名 ,Maven Surefire 插件会自动运行 JUnit 5 测试。执行 MultisigServiceTest 单个测试类(无需写.java后缀):
    mvn test -Dtest=MultisigServiceTest

执行结果:

点击查看
===== 全局初始化 =====

暂不测试,待优化逻辑
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
===== 全局清理 =====

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