QA之二 - 单元测试-- JaCoCo

一、是什么
JaCoCo 是 Java 生态最主流的代码覆盖率工具,能统计单元测试对代码的覆盖程度(哪些代码被执行、哪些没被执行),生成可视化报告,帮你发现测试遗漏的分支 / 代码行。

覆盖维度

  • 行覆盖(Line):代码行是否被执行(最常用);
  • 分支覆盖(Branch):条件分支是否全覆盖(if/else、switch);
  • 方法覆盖(Method):方法是否被调用过;
  • 类覆盖(Class):类是否有至少一个方法被调用;

二、核心原理
JaCoCo 基于字节码插桩(Instrumentation) 实现,无需修改源代码:

  • 测试执行前:JaCoCo 对被测类的字节码插入 “探针”(统计执行次数);
  • 测试执行中:探针记录每个代码行 / 分支的执行情况;
  • 测试执行后:JaCoCo 收集探针数据,生成 HTML/XML/CSV 格式的覆盖率报告。

1、什么是字节码插桩?

  • 官方定义:字节码插桩是在不修改源代码的前提下,对编译后的 Java 字节码(.class 文件)进行动态 / 静态修改,插入额外的代码(“探针”/“钩子”),以实现监控、统计、调试等功能的技术。
  • 大白话:把 Java 字节码看作 “半成品程序”,插桩就是在这个半成品里 “偷偷加代码”—— 比如在方法开头加一行 “统计执行次数”,在 if 分支加一行 “记录是否执行”,但源码完全不变。

2、常见使用场景:测试 / 监控工具

  • JaCoCo:插桩统计代码行 / 分支的执行次数(覆盖率);
  • Mockito:插桩生成 Mock 对象的字节码(模拟外部依赖);
  • Arthas:动态插桩监控线上应用的方法调用、参数、返回值;
  • APM 工具(如 SkyWalking):插桩收集调用链路、性能数据。

3、插桩分类
根据插桩的执行时机,分为 静态插桩 和 动态插桩,二者各有适用场景:

  • 静态插桩(编译后 / 打包前修改字节码)

    • 在代码编译完成后、部署运行前,直接修改 .class 文件或 .jar 包中的字节码,生成新的字节码文件。
    • 优点:修改一次,多次运行生效;性能无实时损耗;
    • 缺点:无法动态调整,需重新打包 / 部署;
    • 工具:ASM、Javassist、ByteBuddy(静态模式);
    • 典型应用:JaCoCo 离线插桩、代码混淆工具。
  • 动态插桩(运行时修改字节码)

    • 在 JVM 加载类的过程中(类加载的 transform 阶段),动态修改字节码,无需修改物理 .class 文件。
    • 优点:无需重新打包 / 部署,运行时动态生效;可按需开启 / 关闭;
    • 缺点:类加载时插桩有轻微性能损耗;无法修改已加载的类(除非重加载);
    • 工具:Java Agent、ByteBuddy(动态模式)、Arthas、BTrace;
    • 典型应用:JaCoCo 运行时插桩、Mockito 动态生成 Mock 对象、Arthas 线上监控。

核心工具对比(JaCoCo 用 ASM,Mockito 用 ByteBuddy)

  • JaCoCo:底层用 ASM 实现静态 / 动态插桩,插入 “探针” 统计代码执行次数;
  • Mockito:用 ByteBuddy 动态生成 Mock 对象的字节码(无需手动创建类);
  • Arthas:基于 Java Agent + ASM 实现线上动态插桩,监控方法调用。

三、怎么用?
1、在 pom.xml 中配置 JaCoCo 插件

点击查看代码
<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>Test2</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>-->

      <!-- 1. JaCoCo 核心插件(生成覆盖率报告) -->
      <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.8.12</version> <!-- 最新稳定版 -->
		<configuration>
          <excludes>
            <!-- 排除配置类 -->
            <exclude>com/multisig/config/**/*</exclude>
            <!-- 排除工具类常量 -->
            <exclude>com/multisig/constants/**/*</exclude>
            <!-- 排除测试专用代码 -->
            <exclude>**/*Test*.class</exclude>
          </excludes>
        </configuration>
        <executions>
          <!-- 执行测试时收集覆盖率数据 -->
          <execution>
            <id>jacoco-prepare-agent</id>
            <goals>
              <goal>prepare-agent</goal>
            </goals>
          </execution>
          <!-- 测试完成后生成覆盖率报告 -->
          <execution>
            <id>jacoco-report</id>
            <phase>test</phase> <!-- 绑定到 mvn test 阶段 -->
            <goals>
              <goal>report</goal>
            </goals>
          </execution>
          <!-- 可选:强制执行覆盖率规则(如行覆盖率≥80%) -->
          <execution>
            <id>jacoco-check</id>
            <goals>
              <goal>check</goal>
            </goals>
            <configuration>
              <rules>
                <!-- 对所有代码的行覆盖率要求 ≥80% -->
                <rule>
                  <element>BUNDLE</element>
                  <limits>
                    <limit>
                      <counter>LINE</counter>
                      <value>COVEREDRATIO</value>
                      <minimum>0.8</minimum> <!-- 80% 行覆盖 -->
                    </limit>
                    <!-- 分支覆盖率 ≥70% -->
                    <limit>
                      <counter>BRANCH</counter>
                      <value>COVEREDRATIO</value>
                      <minimum>0.7</minimum>
                    </limit>
                  </limits>
                </rule>
              </rules>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <!-- 2. 确保 Maven Surefire 插件适配 JUnit 5 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.1.2</version>
        <configuration>
          <includes>
            <include>**/*Test.java</include> <!-- 执行所有测试类 -->
          </includes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

2、被测代码

点击查看代码
package com.example;
public class MultisigService {
    private RpcClient rpcClient;
    private int threshold = 2; // 多签门限

    public MultisigService(RpcClient rpcClient) {
        this.rpcClient = rpcClient;
    }

    // 核心方法:执行多签交易(含多个分支/异常)
    public boolean executeMultisigTx(String txId, int signatureCount) {
        // 分支1:空值校验
        if (txId == null || txId.isEmpty()) {
            return false;
        }

        // 分支2:签名数校验
        if (signatureCount < threshold) {
            return false;
        }

        // 分支3:RPC调用(含异常)
        try {
            return rpcClient.sendTx(txId);
        } catch (RuntimeException e) {
            // 异常分支
            return false;
        }
    }

    // 辅助方法:获取门限(未被测试覆盖)
    public int getThreshold() {
        return this.threshold;
    }
}

package com.example;

public interface RpcClient {
    boolean sendTx(String txId);
}

3、单元测试代码(JUnit 5 + Mockito)

点击查看代码
package com.example;


import org.junit.jupiter.api.DisplayName;
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;

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

@ExtendWith(MockitoExtension.class)
class MultisigServiceTest {
    @Mock
    private RpcClient rpcClient;

    @InjectMocks
    private MultisigService multisigService;

    @Test
    @DisplayName("测试txId为空时返回false")
    void testEmptyTxId() {
        assertFalse(multisigService.executeMultisigTx("", 2));
    }

    @Test
    @DisplayName("测试签名数不足时返回false")
    void testInsufficientSignature() {
        assertFalse(multisigService.executeMultisigTx("tx-001", 1));
    }

    @Test
    @DisplayName("测试RPC返回成功时返回true")
    void testRpcSuccess() {
        when(rpcClient.sendTx("tx-002")).thenReturn(true);
        assertTrue(multisigService.executeMultisigTx("tx-002", 2));
    }

    // 补充测试:RPC抛异常分支
    @Test
    @DisplayName("测试RPC抛异常时返回false")
    void testRpcException() {
        when(rpcClient.sendTx("tx-003")).thenThrow(new RuntimeException("RPC超时"));
        assertFalse(multisigService.executeMultisigTx("tx-003", 2));
    }

    // 补充测试:getThreshold方法
    @Test
    @DisplayName("测试获取门限方法")
    void testGetThreshold() {
        assertEquals(2, multisigService.getThreshold());
    }
}

4、执行命令

  • mvn clean test:执行单元测试 + 生成 JaCoCo 覆盖率报告
  • mvn jacoco:report:单独生成覆盖率报告(无需重新执行测试)
  • mvn jacoco:check:单独检查覆盖率是否满足规则

5、报告生成位置
JaCoCo 会在项目目录下生成报告:

点击查看代码
target/
└── site/
    └── jacoco/
        ├── index.html  # 核心HTML报告(打开即可可视化查看)
        ├── jacoco.xml  # XML报告(CI/CD用)
        └── jacoco.csv  # CSV报告(数据分析用)

6、JaCoCo 报告解读
打开 target/site/jacoco/index.html,核心界面分为 3 部分
image

6.1)概览页(项目级覆盖率)
image

6.2)类详情页
代码行标注颜色:
✅ 绿色:已覆盖(如 txId.isEmpty() 行);
❌ 红色:未覆盖(如 catch (RuntimeException e) 行、getThreshold() 方法);
⚠️ 黄色:部分覆盖(如 if (signatureCount < threshold) 仅测试了 true 分支?不,本例中测试了 true,false 未测?需看具体)。

6.3)分支详情
if (signatureCount < threshold):true 分支被测试(签名数 = 1),false 分支被测试(签名数 = 2)→ 100% 覆盖;
try/catch:try 块被测试(RPC 成功),catch 块未测试 → 50% 覆盖;
if (txId == null || txId.isEmpty()):true 分支被测试(空 txId),false 分支被测试(非空 txId)→ 100% 覆盖。

7、排除无需覆盖的代码
项目中,部分代码(如配置类、工具类常量)无需统计覆盖率,可在 JaCoCo 中排除。

7.1)JaCoCo 排除的核心场景
代码完全无需统计覆盖率,排除后能让覆盖率指标更精准:

排除场景 典型示例
配置类 / 常量类 MultisigConfig(多签服务配置)、ChainConstants(链上常量)
工具类静态常量 / 空方法 SignatureUtils 中的常量、RpcUtils 中的空构造方法
测试专用代码 TestDataGenerator(测试数据生成)、MockRpcClient
第三方依赖 / 生成代码 / 空方法 OpenAPI 生成的 API 层代码、自动生成的实体类
特定方法(如 getter/setter) MultisigService.getThreshold()(简单取值方法)
单行代码(如日志 / 注释) 方法内的日志打印行、异常捕获后的兜底行

7.2) 如何配置
(1) 包 / 类级排除

点pom.xml
<!-- Maven 配置排除 -->
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.12</version>
  <configuration>
    <!-- 核心:排除配置 -->
        <excludes>
          <!-- 1. 排除整个包(通配符:** 匹配任意子包,* 匹配任意类) -->
          <exclude>com/multisig/config/**/*</exclude>          <!-- 排除配置包 -->
          <exclude>com/multisig/constants/**/*</exclude>       <!-- 排除常量包 -->
          
          <!-- 2. 排除指定类(精确匹配) -->
          <exclude>com/multisig/MockRpcClient.class</exclude>  <!-- 排除Mock类 -->
          
          <!-- 3. 排除匹配规则的类(通配符) -->
          <exclude>com/multisig/*DTO.class</exclude>           <!-- 排除所有DTO实体类 -->
          <exclude>com/multisig/*Utils.class</exclude>         <!-- 排除所有工具类 -->
          
          <!-- 4. 排除测试生成代码 -->
          <exclude>com/multisig/api/**/*</exclude>             <!-- 排除OpenAPI生成的API层 -->
        </excludes>
  </configuration>
  <!-- 其余executions配置不变 -->
</plugin>

配置通配符规则

通配符 含义 示例 匹配结果
** 匹配任意层级的目录 com/multisig/**/* 匹配 com/multisig 下所有类
* 匹配任意字符(无层级) com/multisig/*Utils.class 匹配 com/multisig/RpcUtils.class
? 匹配单个字符 com/multisig/M?ck.class 匹配 com/multisig/Mock.class

(2) 类 / 方法级排除(注解方式,精准控制)
JaCoCo 支持通过自定义注解标记需要排除的类 / 方法,无需修改插件配置,这种不推荐。

(3) 单行代码排除(精准过滤单行)
针对方法内无需统计的单行代码(如日志、异常兜底),JaCoCo 支持通过特殊注释排除单行:
在需要排除的代码行末尾添加注释:// $COVERAGE-IGNORE$

点击查看代码
public class MultisigService {
    public boolean executeMultisigTx(String txId, int signatureCount) {
        if (txId == null || txId.isEmpty()) {
            return false;
        }

        try {
            return rpcClient.sendTx(txId);
        } catch (RuntimeException e) {
            // 排除日志行(无需统计覆盖率)
            log.error("RPC调用失败", e); // $COVERAGE-IGNORE$
            // 排除兜底返回行(简单逻辑,无需测试)
            return false; // $COVERAGE-IGNORE$
        }
    }
}

(4) 报告级排除(仅过滤报告,不影响插桩)
若仅需在覆盖率报告中隐藏某些代码(但仍会插桩统计),可通过报告过滤实现:

点击查看代码
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.12</version>
  <executions>
    <execution>
      <id>jacoco-report</id>
      <phase>test</phase>
      <goals><goal>report</goal></goals>
      <configuration>
        <!-- 仅报告中排除,插桩仍会执行 -->
        <excludes>
          <exclude>com/multisig/utils/**/*</exclude>
        </excludes>
      </configuration>
    </execution>
  </executions>
</plugin>
posted @ 2026-02-28 22:10  cac2020  阅读(1)  评论(0)    收藏  举报