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的 true
  • score = 75 → 走分支1的 false + 分支2的 true
  • score = 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 自动排除标注了 @Generatedjavax.annotation.Generatedlombok.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 verifymvn 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 通过字节码探针记录测试执行了哪些代码,生成覆盖率报告并可设置门禁阈值,确保团队的测试覆盖率不会随时间退化。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐