JaCoCo 代码覆盖率完全指南
JaCoCo 代码覆盖率完全指南
一、什么是代码覆盖率?
代码覆盖率是衡量测试对代码执行程度的指标。它回答一个问题:你写的单元测试到底覆盖了多少代码?
你的项目有 1000 行代码
单元测试执行了其中 800 行
覆盖率 = 800 / 1000 = 80%
覆盖率高不代表没有 Bug(测试可能执行了代码但没验证结果),但覆盖率低一定意味着有未被测试保护的代码。
二、覆盖率的类型
| 类型 | 含义 | 示例 |
|---|---|---|
| 行覆盖率(Line) | 有多少行代码被执行了 | 100 行执行了 80 行 = 80% |
| 分支覆盖率(Branch) | if/else 的每个分支都测到了吗 | 10 个 if 有 7 个两边都测了 = 70% |
| 方法覆盖率(Method) | 有多少方法被调用了 | 50 个方法测了 45 个 = 90% |
| 类覆盖率(Class) | 有多少类被测试触及了 | 20 个类测了 18 个 = 90% |
| 指令覆盖率(Instruction) | 字节码指令级别的覆盖 | 最细粒度 |
最重要的两个:行覆盖率 + 分支覆盖率。
分支覆盖率示例
public String getLevel(int score) {
if (score >= 90) { // 分支1: true / false
return "A";
} else if (score >= 60) { // 分支2: true / false
return "B";
} else {
return "C";
}
}
要达到 100% 分支覆盖率,需要测试:
score = 95→ 走分支1的 truescore = 75→ 走分支1的 false + 分支2的 truescore = 40→ 走分支1的 false + 分支2的 false
三、JaCoCo 简介
3.1 它是什么?
JaCoCo(Java Code Coverage)是 Java 生态最主流的代码覆盖率工具。它通过在 JVM 运行时植入字节码探针来统计代码执行情况,对源代码零侵入。
3.2 工作原理
1. 构建阶段:JaCoCo Agent 植入字节码探针
2. 测试阶段:执行单元测试,Agent 记录哪些代码被执行了
3. 报告阶段:生成 HTML/XML/CSV 覆盖率报告
┌──────────────────────────────────────────────────┐
│ JaCoCo 工作流 │
├──────────────────────────────────────────────────┤
│ │
│ mvn test │
│ │ │
│ ↓ ① prepare-agent │
│ ┌──────────────┐ │
│ │ JaCoCo Agent │ → 植入探针到 class 文件 │
│ └──────┬───────┘ │
│ ↓ ② 执行测试 │
│ ┌──────────────┐ │
│ │ JUnit Tests │ → 运行时记录执行路径 │
│ └──────┬───────┘ │
│ ↓ ③ report │
│ ┌──────────────┐ │
│ │ 覆盖率报告 │ → target/site/jacoco/ │
│ │ HTML + XML │ │
│ └──────────────┘ │
│ │
└──────────────────────────────────────────────────┘
四、Maven 集成配置
4.1 基础配置
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<!-- ① 测试前:植入 Agent -->
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- ② 测试后:生成报告 -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
4.2 带覆盖率门禁的配置
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<!-- 植入 Agent -->
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- 生成报告 -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<!-- ③ 覆盖率门禁检查(不达标则构建失败) -->
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<!-- 规则1:整体覆盖率 -->
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum> <!-- 行覆盖率 ≥ 80% -->
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum> <!-- 分支覆盖率 ≥ 70% -->
</limit>
</limits>
</rule>
<!-- 规则2:每个类的覆盖率 -->
<rule>
<element>CLASS</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum> <!-- 每个类至少 60% -->
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
<!-- 排除不需要统计的类 -->
<configuration>
<excludes>
<exclude>**/entity/**</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/config/**</exclude>
<exclude>**/enums/**</exclude>
<exclude>**/*Application.*</exclude>
<exclude>**/generated/**</exclude>
</excludes>
</configuration>
</plugin>
4.3 配置项说明
element(规则应用的范围)
| 值 | 含义 |
|---|---|
BUNDLE |
整个项目(所有类合计) |
PACKAGE |
每个包 |
CLASS |
每个类 |
METHOD |
每个方法 |
counter(计数器类型)
| 值 | 含义 |
|---|---|
LINE |
代码行 |
BRANCH |
分支 |
METHOD |
方法 |
CLASS |
类 |
INSTRUCTION |
字节码指令 |
COMPLEXITY |
圈复杂度 |
value(度量方式)
| 值 | 含义 |
|---|---|
COVEREDRATIO |
覆盖比率(0.0 ~ 1.0) |
COVEREDCOUNT |
已覆盖数量 |
MISSEDCOUNT |
未覆盖数量 |
MISSEDRATIO |
未覆盖比率 |
TOTALCOUNT |
总计数量 |
五、常用命令
# 运行测试并生成覆盖率报告
mvn clean test jacoco:report
# 报告位置:target/site/jacoco/index.html
# 运行测试 + 覆盖率 + 门禁检查(不达标则失败)
mvn clean verify
# 只查看报告不做门禁检查
mvn clean test
# 报告自动生成(因为 report 绑定在 test 阶段)
# 跳过测试(也就不会有覆盖率)
mvn clean package -DskipTests
六、报告解读
6.1 HTML 报告结构
打开 target/site/jacoco/index.html:
项目总览
├── com.example.order(包级别)
│ ├── OrderServiceImpl(类级别)
│ │ ├── createOrder()(方法级别)
│ │ ├── validateRequest()
│ │ └── buildOrder()
│ └── OrderController
└── com.example.product
└── ProductServiceImpl
6.2 颜色含义
| 颜色 | 含义 |
|---|---|
| 🟢 绿色 | 完全覆盖(该行/分支被测试执行了) |
| 🟡 黄色 | 部分覆盖(分支只覆盖了一部分) |
| 🔴 红色 | 未覆盖(该行/分支从未被测试执行) |
6.3 报告示例(源码视图)
public String getDiscount(User user) {
🟢 if (user == null) { // 分支:true ✓, false ✓
🟢 return "0%";
}
🟡 if (user.isVip() && user.getAge() > 18) { // 部分覆盖:只测了 true
🟢 return "20%";
}
🔴 if (user.getPoints() > 1000) { // 从未执行
🔴 return "10%";
}
🟢 return "5%";
}
七、完整通用示例
7.1 项目结构
my-service/
├── pom.xml
├── src/
│ ├── main/java/com/example/
│ │ ├── service/
│ │ │ ├── OrderService.java
│ │ │ └── impl/OrderServiceImpl.java
│ │ ├── entity/Order.java ← 排除
│ │ └── dto/OrderRequest.java ← 排除
│ └── test/java/com/example/
│ └── service/
│ └── OrderServiceTest.java
└── target/
└── site/
└── jacoco/
└── index.html ← 覆盖率报告
7.2 被测试的 Service
package com.example.service.impl;
import com.example.entity.Order;
import com.example.exception.BusinessException;
import com.example.repository.OrderRepository;
import com.example.service.OrderService;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
private static final int MAX_QUANTITY = 99;
private static final BigDecimal FREE_SHIPPING_THRESHOLD = new BigDecimal("99.00");
private final OrderRepository orderRepository;
public OrderServiceImpl(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public Order createOrder(Long userId, Long productId, int quantity, BigDecimal unitPrice) {
// 参数校验
if (userId == null) {
throw new BusinessException("用户ID不能为空");
}
if (productId == null) {
throw new BusinessException("商品ID不能为空");
}
if (quantity <= 0 || quantity > MAX_QUANTITY) {
throw new BusinessException("数量必须在1-99之间");
}
if (unitPrice == null || unitPrice.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("单价必须大于0");
}
// 计算总价
BigDecimal totalAmount = unitPrice.multiply(BigDecimal.valueOf(quantity));
// 计算运费
BigDecimal shippingFee = calculateShippingFee(totalAmount);
// 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setUnitPrice(unitPrice);
order.setTotalAmount(totalAmount);
order.setShippingFee(shippingFee);
order.setPayAmount(totalAmount.add(shippingFee));
order.setStatus("CREATED");
order.setCreateTime(LocalDateTime.now());
orderRepository.save(order);
log.info("订单创建成功, orderId={}, payAmount={}", order.getId(), order.getPayAmount());
return order;
}
@Override
public BigDecimal calculateShippingFee(BigDecimal totalAmount) {
if (totalAmount == null) {
return new BigDecimal("10.00");
}
// 满 99 免运费
if (totalAmount.compareTo(FREE_SHIPPING_THRESHOLD) >= 0) {
return BigDecimal.ZERO;
}
// 不满 99 收 10 元运费
return new BigDecimal("10.00");
}
@Override
public String getOrderStatus(Long orderId) {
if (orderId == null) {
throw new BusinessException("订单ID不能为空");
}
Order order = orderRepository.findById(orderId);
if (order == null) {
return null;
}
return order.getStatus();
}
}
7.3 覆盖率达标的单元测试
package com.example.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import com.example.entity.Order;
import com.example.exception.BusinessException;
import com.example.repository.OrderRepository;
import com.example.service.impl.OrderServiceImpl;
import java.math.BigDecimal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
private OrderService orderService;
@BeforeEach
void setUp() {
orderService = new OrderServiceImpl(orderRepository);
}
// ========== createOrder 测试 ==========
@Nested
@DisplayName("createOrder - 参数校验")
class CreateOrderValidation {
@Test
@DisplayName("用户ID为空时抛异常")
void shouldThrowWhenUserIdNull() {
BusinessException ex = assertThrows(BusinessException.class,
() -> orderService.createOrder(null, 1L, 1, new BigDecimal("10")));
assertEquals("用户ID不能为空", ex.getMessage());
}
@Test
@DisplayName("商品ID为空时抛异常")
void shouldThrowWhenProductIdNull() {
BusinessException ex = assertThrows(BusinessException.class,
() -> orderService.createOrder(1L, null, 1, new BigDecimal("10")));
assertEquals("商品ID不能为空", ex.getMessage());
}
@Test
@DisplayName("数量为0时抛异常")
void shouldThrowWhenQuantityZero() {
BusinessException ex = assertThrows(BusinessException.class,
() -> orderService.createOrder(1L, 1L, 0, new BigDecimal("10")));
assertEquals("数量必须在1-99之间", ex.getMessage());
}
@Test
@DisplayName("数量超过99时抛异常")
void shouldThrowWhenQuantityExceedsMax() {
assertThrows(BusinessException.class,
() -> orderService.createOrder(1L, 1L, 100, new BigDecimal("10")));
}
@Test
@DisplayName("单价为null时抛异常")
void shouldThrowWhenUnitPriceNull() {
assertThrows(BusinessException.class,
() -> orderService.createOrder(1L, 1L, 1, null));
}
@Test
@DisplayName("单价为0时抛异常")
void shouldThrowWhenUnitPriceZero() {
assertThrows(BusinessException.class,
() -> orderService.createOrder(1L, 1L, 1, BigDecimal.ZERO));
}
}
@Nested
@DisplayName("createOrder - 正常流程")
class CreateOrderSuccess {
@Test
@DisplayName("正常创建订单 - 满99免运费")
void shouldCreateOrderWithFreeShipping() {
// Arrange
doAnswer(invocation -> {
Order order = invocation.getArgument(0);
order.setId(1L);
return null;
}).when(orderRepository).save(any(Order.class));
// Act
Order order = orderService.createOrder(1L, 100L, 2, new BigDecimal("50.00"));
// Assert
assertNotNull(order);
assertEquals(1L, order.getUserId());
assertEquals(100L, order.getProductId());
assertEquals(2, order.getQuantity());
assertEquals(new BigDecimal("100.00"), order.getTotalAmount());
assertEquals(BigDecimal.ZERO, order.getShippingFee()); // 满99免运费
assertEquals(new BigDecimal("100.00"), order.getPayAmount());
assertEquals("CREATED", order.getStatus());
assertNotNull(order.getCreateTime());
verify(orderRepository, times(1)).save(any(Order.class));
}
@Test
@DisplayName("正常创建订单 - 不满99收运费")
void shouldCreateOrderWithShippingFee() {
// Act
Order order = orderService.createOrder(1L, 100L, 1, new BigDecimal("50.00"));
// Assert
assertEquals(new BigDecimal("50.00"), order.getTotalAmount());
assertEquals(new BigDecimal("10.00"), order.getShippingFee()); // 不满99收10元
assertEquals(new BigDecimal("60.00"), order.getPayAmount());
}
}
// ========== calculateShippingFee 测试 ==========
@Nested
@DisplayName("calculateShippingFee")
class CalculateShippingFee {
@Test
@DisplayName("金额为null返回10元运费")
void shouldReturn10WhenAmountNull() {
assertEquals(new BigDecimal("10.00"), orderService.calculateShippingFee(null));
}
@Test
@DisplayName("满99免运费")
void shouldReturnZeroWhenAboveThreshold() {
assertEquals(BigDecimal.ZERO, orderService.calculateShippingFee(new BigDecimal("99.00")));
assertEquals(BigDecimal.ZERO, orderService.calculateShippingFee(new BigDecimal("100.00")));
}
@Test
@DisplayName("不满99收10元运费")
void shouldReturn10WhenBelowThreshold() {
assertEquals(new BigDecimal("10.00"), orderService.calculateShippingFee(new BigDecimal("98.99")));
assertEquals(new BigDecimal("10.00"), orderService.calculateShippingFee(new BigDecimal("1.00")));
}
}
// ========== getOrderStatus 测试 ==========
@Nested
@DisplayName("getOrderStatus")
class GetOrderStatus {
@Test
@DisplayName("订单ID为空抛异常")
void shouldThrowWhenOrderIdNull() {
assertThrows(BusinessException.class, () -> orderService.getOrderStatus(null));
}
@Test
@DisplayName("订单不存在返回null")
void shouldReturnNullWhenNotFound() {
when(orderRepository.findById(999L)).thenReturn(null);
assertNull(orderService.getOrderStatus(999L));
}
@Test
@DisplayName("订单存在返回状态")
void shouldReturnStatus() {
Order order = new Order();
order.setStatus("PAID");
when(orderRepository.findById(1L)).thenReturn(order);
assertEquals("PAID", orderService.getOrderStatus(1L));
}
}
}
7.4 运行结果
$ mvn clean verify
[INFO] --- jacoco:0.8.12:report ---
[INFO] Analyzed bundle 'my-service' with 3 classes
[INFO] --- jacoco:0.8.12:check ---
[INFO] All coverage checks have been met.
# 查看 target/site/jacoco/index.html:
# OrderServiceImpl: 行覆盖率 95%, 分支覆盖率 100%
八、排除策略
8.1 哪些代码应该排除?
| 类型 | 原因 | 配置 |
|---|---|---|
| Entity/DTO | 只有 getter/setter,用 Lombok 生成 | **/entity/**, **/dto/** |
| 配置类 | Spring 配置,无业务逻辑 | **/config/** |
| 枚举 | 简单值定义 | **/enums/** |
| 启动类 | main 方法,无需测试 | **/*Application.* |
| 自动生成代码 | MyBatis Generator 等 | **/generated/** |
| 常量类 | 只有 static final | **/constants/** |
8.2 排除配置方式
Maven 插件配置(推荐)
<configuration>
<excludes>
<exclude>**/entity/**</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/config/**</exclude>
<exclude>**/enums/**</exclude>
<exclude>**/*Application.*</exclude>
</excludes>
</configuration>
注解排除(类级别)
// 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Generated {
}
// 使用注解标记
@Generated
public class SomeGeneratedClass {
// JaCoCo 会自动排除有 @Generated 注解的类
}
JaCoCo 自动排除标注了
@Generated(javax.annotation.Generated或lombok.Generated)注解的代码。Lombok 生成的方法自带此注解,无需额外配置。
九、与 CI/CD 集成
9.1 GitLab CI
test:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- mvn clean verify
artifacts:
paths:
- target/site/jacoco/
reports:
coverage_report:
coverage_format: cobertura
path: target/site/jacoco/jacoco.xml
coverage: '/Total.*?([0-9]{1,3})%/' # 从日志中提取覆盖率数字
9.2 与 SonarQube 集成
SonarQube 读取 JaCoCo 的 XML 报告来展示覆盖率:
<properties>
<!-- 告诉 SonarQube 覆盖率报告的位置 -->
<sonar.coverage.jacoco.xmlReportPaths>
${project.build.directory}/site/jacoco/jacoco.xml
</sonar.coverage.jacoco.xmlReportPaths>
<!-- SonarQube 中也排除这些类的覆盖率统计 -->
<sonar.coverage.exclusions>
**/entity/**,
**/dto/**,
**/config/**,
**/enums/**,
**/*Application.java
</sonar.coverage.exclusions>
</properties>
完整执行命令:
# 测试 + 覆盖率 + 上传 SonarQube
mvn clean verify sonar:sonar \
-Dsonar.host.url=http://sonar.example.com:9000 \
-Dsonar.token=sqp_xxxx
十、多模块项目配置
对于多模块 Maven 项目,需要聚合各模块的覆盖率:
<!-- 父 pom.xml -->
<modules>
<module>module-api</module>
<module>module-service</module>
<module>module-dao</module>
<module>module-coverage</module> <!-- 专门用于聚合报告的模块 -->
</modules>
<!-- module-coverage/pom.xml -->
<dependencies>
<!-- 依赖所有要统计的模块 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>module-service</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>report-aggregate</id>
<phase>verify</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
十一、提升覆盖率的实践技巧
11.1 哪些代码值得优先测试?
| 优先级 | 代码类型 | 原因 |
|---|---|---|
| 🔴 高 | Service 层业务逻辑 | 核心业务,Bug 影响大 |
| 🔴 高 | 工具类/计算逻辑 | 被广泛引用,影响面大 |
| 🟡 中 | Controller 参数校验 | 防止非法请求 |
| 🟡 中 | 异常分支 | 确保错误处理正确 |
| 🟢 低 | Entity/DTO | Lombok 生成,无逻辑 |
| 🟢 低 | 配置类 | 框架驱动,行为固定 |
11.2 测试模式
// 模式1:正常路径(Happy Path)
@Test
void createOrder_success() { ... }
// 模式2:边界条件
@Test
void createOrder_quantityIsOne() { ... }
@Test
void createOrder_quantityIsMax() { ... }
// 模式3:异常路径
@Test
void createOrder_userIdNull_throwsException() { ... }
@Test
void createOrder_stockInsufficient_throwsException() { ... }
// 模式4:分支覆盖
@Test
void calculateFee_aboveThreshold_freeShipping() { ... }
@Test
void calculateFee_belowThreshold_chargesFee() { ... }
@Test
void calculateFee_exactThreshold_freeShipping() { ... }
11.3 覆盖率目标建议
| 代码类型 | 建议覆盖率 |
|---|---|
| 核心业务 Service | ≥ 90% |
| 工具类 | ≥ 95% |
| Controller | ≥ 70% |
| 项目整体 | ≥ 80% |
| 新代码(增量) | ≥ 80% |
十二、常见问题 FAQ
Q1: Lombok 生成的 getter/setter 影响覆盖率怎么办?
JaCoCo 0.8.0+ 版本会自动忽略带有 @lombok.Generated 注解的代码。Lombok 1.18+ 默认给生成的方法加这个注解,所以不需要额外配置。
如果你用的是旧版本 Lombok,可以在项目根目录创建 lombok.config:
# lombok.config
lombok.addLombokGeneratedAnnotation = true
Q2: 覆盖率达到 100% 有必要吗?
没必要,也不现实。追求 100% 会带来:
- 测试维护成本暴增
- 为了覆盖率写无意义的测试(如测试 getter)
- 收益递减(从 80% 到 100% 花的精力远超 0% 到 80%)
推荐目标:整体 80%,核心逻辑 90%。
Q3: 覆盖率高但还是有 Bug,为什么?
覆盖率只衡量"代码被执行了",不衡量"结果被验证了":
// 覆盖率 100%,但完全没有验证
@Test
void testCreateOrder() {
orderService.createOrder(request); // 执行了,但没 assert 任何东西
}
// 正确做法:必须有断言
@Test
void testCreateOrder() {
Order order = orderService.createOrder(request);
assertEquals("CREATED", order.getStatus()); // 验证状态
assertEquals(new BigDecimal("100"), order.getTotal()); // 验证金额
verify(repository).save(any()); // 验证调用
}
Q4: mvn verify 和 mvn test 的区别?
| 命令 | 执行的生命周期阶段 | 效果 |
|---|---|---|
mvn test |
compile → test | 编译 + 运行测试 + 生成报告 |
mvn verify |
compile → test → package → verify | 上述 + 打包 + 执行 check 门禁 |
如果 JaCoCo 的 check 绑定在 verify 阶段,只有 mvn verify 才会触发覆盖率门禁检查。
Q5: 如何只看某个包/类的覆盖率?
打开 HTML 报告后逐层点击即可。命令行可以用:
# 检查特定包的覆盖率
mvn jacoco:check -Djacoco.includes="com/example/service/**"
或在 check 规则中指定包:
<rule>
<element>PACKAGE</element>
<includes>
<include>com.example.service</include>
</includes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
Q6: Spring Boot 测试时 JaCoCo 不统计怎么办?
确保 prepare-agent 在测试前执行。如果使用了 maven-surefire-plugin 自定义了 argLine,需要保留 JaCoCo 的 agent 参数:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- 关键:保留 JaCoCo 注入的 argLine -->
<argLine>${argLine} -Xmx1024m</argLine>
</configuration>
</plugin>
${argLine} 是 JaCoCo 的 prepare-agent 自动设置的变量,包含 -javaagent:...jacoco.jar=... 参数。
Q7: 集成测试和单元测试的覆盖率能合并吗?
可以。分别生成 exec 文件,最后合并:
<execution>
<id>merge-results</id>
<phase>verify</phase>
<goals>
<goal>merge</goal>
</goals>
<configuration>
<fileSets>
<fileSet>
<directory>${project.build.directory}</directory>
<includes>
<include>jacoco.exec</include> <!-- 单元测试 -->
<include>jacoco-it.exec</include> <!-- 集成测试 -->
</includes>
</fileSet>
</fileSets>
<destFile>${project.build.directory}/jacoco-merged.exec</destFile>
</configuration>
</execution>
Q8: Mapper 接口需要测试覆盖吗?
MyBatis Mapper 接口本身无实现代码(实现在 XML/注解中),JaCoCo 统计不到它的覆盖率。通常做法:
- 在 JaCoCo 排除中加
**/dao/**或**/*Mapper.* - 如果需要测 SQL 正确性,写集成测试配合内存数据库(H2)
十三、Gradle 项目配置参考
如果项目使用 Gradle 而非 Maven:
// build.gradle
plugins {
id 'java'
id 'jacoco'
}
jacoco {
toolVersion = "0.8.12"
}
test {
useJUnitPlatform()
finalizedBy jacocoTestReport // 测试完自动生成报告
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true // SonarQube 需要 XML
html.required = true // 人看 HTML
csv.required = false
}
// 排除
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
'**/entity/**',
'**/dto/**',
'**/config/**',
'**/*Application*'
])
}))
}
}
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80
}
}
rule {
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.70
}
}
}
}
// 绑定门禁到 check 任务
check.dependsOn jacocoTestCoverageVerification
# 运行测试 + 报告
./gradlew test jacocoTestReport
# 运行测试 + 门禁检查
./gradlew check
十四、总结
| 要点 | 说明 |
|---|---|
| 工具 | JaCoCo(Java 生态标准) |
| 目标 | 整体 ≥ 80%,核心逻辑 ≥ 90% |
| 关键指标 | 行覆盖率 + 分支覆盖率 |
| 排除范围 | Entity、DTO、Config、Enum、启动类 |
| 门禁配置 | check goal 绑定 verify 阶段 |
| 报告位置 | target/site/jacoco/index.html |
| CI 集成 | 生成 XML 供 SonarQube / GitLab 读取 |
| 核心命令 | mvn clean verify(测试+报告+门禁) |
一句话总结:JaCoCo 通过字节码探针记录测试执行了哪些代码,生成覆盖率报告并可设置门禁阈值,确保团队的测试覆盖率不会随时间退化。
更多推荐




所有评论(0)