QA之二 - 单元测试--JUnit5

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

执行结果:

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

暂不测试,待优化逻辑
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
----- 测试方法初始化 -----
----- 测试方法清理 -----
===== 全局清理 =====
posted @ 2026-02-24 23:25  cac2020  阅读(4)  评论(0)    收藏  举报