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

推荐订阅源

A
About on SuperTechFans
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Security Blog
Microsoft Security Blog
aimingoo的专栏
aimingoo的专栏
I
InfoQ
C
Check Point Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
Apple Machine Learning Research
Apple Machine Learning Research
Vercel News
Vercel News
Last Week in AI
Last Week in AI
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
Stack Overflow Blog
Stack Overflow Blog
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
B
Blog
T
The Blog of Author Tim Ferriss
H
Help Net Security
云风的 BLOG
云风的 BLOG

博客园 - cac2020

QA之三 - 变异测试 -- PITest QA之二 - 单元测试-- JUnit5 + Mockito 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之二 - 单元测试-- JaCoCo
cac2020 · 2026-02-28 · via 博客园 - cac2020

一、是什么
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>

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