Java 测试 18:Jacoco 代码覆盖率报告(Maven 整合配置)

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长。
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕一个常见的开发话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
Java 测试 18:Jacoco 代码覆盖率报告(Maven 整合配置) 🧪📊
在现代软件开发中,代码质量是保障系统稳定性和可维护性的基石。为了衡量代码质量,尤其是测试覆盖程度,代码覆盖率(Code Coverage)成为了不可或缺的指标。它反映了测试用例对源代码的覆盖范围,帮助开发者识别未被充分测试的代码路径,从而提升软件的可靠性。
JaCoCo(Java Code Coverage)是目前 Java 生态中最受欢迎和广泛使用的代码覆盖率工具之一。它能够精确地测量 Java 程序的执行情况,生成详细的覆盖率报告,包括行覆盖率、分支覆盖率、方法覆盖率等关键指标。通过将 JaCoCo 与 Maven 构建工具集成,我们可以轻松地在构建过程中自动执行覆盖率检查,并生成专业的报告。
本篇文章将深入探讨如何在 Maven 项目中配置和使用 JaCoCo 来生成代码覆盖率报告。我们将从 JaCoCo 的核心概念入手,详细介绍其工作原理,然后逐步演示如何在 Maven 项目中进行整合配置,包括基础配置、高级选项、与 CI/CD 流水线的结合以及最佳实践。通过丰富的代码示例和详细的解释,你将能够掌握如何利用 JaCoCo Maven 插件来自动化地进行代码覆盖率分析,并生成直观、可读的报告,为你的 Java 项目保驾护航。文章中还会穿插 Mermaid 图表来直观地展示配置流程和报告结构,帮助你更好地理解和应用所学知识。我们还将提供一些可以正常访问的外部链接,方便你在实践中进行探索和学习。
什么是代码覆盖率? 🤔
代码覆盖率(Code Coverage)是一种软件测试度量标准,用于衡量测试用例执行了多少比例的源代码。它帮助我们了解测试的完整性,识别测试盲区,从而改进测试策略。
为什么要关注代码覆盖率?
- 提高测试质量:确保测试用例尽可能多地覆盖了代码逻辑,减少遗漏。
- 识别未测试代码:发现那些从未被执行过的代码片段,可能是潜在的风险点或新功能的遗漏。
- 指导测试开发:通过覆盖率报告,可以明确知道哪些部分需要增加更多的测试用例。
- 保障代码质量:高覆盖率通常意味着代码经过了更严格的测试,降低了 Bug 的风险。
- 合规性要求:某些行业或项目可能有特定的覆盖率要求。
常见的覆盖率指标
- 行覆盖率(Line Coverage):执行的代码行数占总代码行数的比例。
- 分支覆盖率(Branch Coverage):执行的分支(if/else, switch 等)数占总分支数的比例。
- 方法覆盖率(Method Coverage):执行的方法数占总方法数的比例。
- 类覆盖率(Class Coverage):执行的类数占总类数的比例。
- 指令覆盖率(Instruction Coverage):执行的指令数占总指令数的比例(较底层)。
什么是 JaCoCo? 🧠
JaCoCo(Java Code Coverage)是由 Eclipse Foundation 维护的一个开源 Java 代码覆盖率工具。它能够以非常高的精度测量 Java 程序的执行情况。
JaCoCo 的核心优势
- 精确测量:JaCoCo 通过字节码增强技术(Bytecode Instrumentation)在运行时收集覆盖率数据,测量非常精确。
- 易于使用:提供了多种集成方式,包括 Maven 插件、Ant 任务、命令行工具和 Eclipse 插件。
- 报告丰富:支持生成 HTML、XML、CSV 等多种格式的报告,方便查看和分析。
- 与主流工具集成良好:与 JUnit、TestNG、Spring Boot 等框架无缝配合。
- 开源免费:完全免费且开源,社区活跃。
JaCoCo 的工作原理
JaCoCo 的核心在于其字节码增强机制。当 Maven 构建项目时,JaCoCo 插件会在编译后的字节码中插入额外的监控代码。这些代码会在程序运行时记录哪些代码行、分支被执行了。运行测试后,JaCoCo 会根据这些记录生成详细的覆盖率报告。
JaCoCo Maven 插件简介 🧱
JaCoCo Maven 插件是将 JaCoCo 集成到 Maven 项目中最常用的方式。它允许我们在构建生命周期中自动执行覆盖率分析,并生成报告。
主要目标(Goals)
prepare-agent:准备代理,用于在运行测试时收集覆盖率数据。prepare-agent-integration:为集成测试准备代理。report:生成覆盖率报告。report-aggregate:聚合多个模块的覆盖率报告。check:根据设定的阈值检查覆盖率是否达标。
配置方式
JaCoCo Maven 插件可以通过 pom.xml 文件中的 <plugin> 标签进行配置。通常,我们会将配置放置在 <build><plugins> 部分。
Maven 项目基础配置 🛠️
在开始配置 JaCoCo 之前,确保你的项目是一个标准的 Maven 项目,并且已经包含了必要的测试框架(如 JUnit 5)。
1. 项目结构
典型的 Maven 项目结构如下:
my-project/ ├── pom.xml ├── src/ │ ├── main/ │ │ └── java/ │ │ └── com/example/ │ │ └── Calculator.java │ └── test/ │ └── java/ │ └── com/example/ │ └── CalculatorTest.java └── target/ 2. 添加 JUnit 5 依赖(如果尚未添加)
<dependencies><!-- 其他依赖... --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.2</version><!-- 使用最新稳定版 --><scope>test</scope></dependency></dependencies>3. 添加 JaCoCo Maven 插件
在 pom.xml 文件的 <build><plugins> 部分添加 JaCoCo 插件:
<build><plugins><!-- 其他插件... --><!-- JaCoCo Maven Plugin --><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><!-- 使用最新稳定版 --><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals></execution></executions></plugin></plugins></build>代码解析:
<groupId>org.jacoco</groupId>和<artifactId>jacoco-maven-plugin</artifactId>:指定插件的坐标。<version>0.8.12</version>:指定插件版本。建议使用最新稳定版。<executions>:定义插件执行的生命周期阶段。prepare-agent:在测试执行前准备 JaCoCo 代理。report:在prepare-package阶段生成报告。你也可以在verify阶段执行。
4. 示例代码
让我们创建一个简单的计算器类和对应的测试类来演示:
Calculator.java
packagecom.example;publicclassCalculator{publicintadd(int a,int b){return a + b;}publicintsubtract(int a,int b){return a - b;}publicintmultiply(int a,int b){return a * b;}publicdoubledivide(int a,int b){if(b ==0){thrownewIllegalArgumentException("Division by zero is not allowed.");}return(double) a / b;}// 一个未被测试的方法 (用于展示覆盖率)publicbooleanisEven(int number){return number %2==0;}}CalculatorTest.java
packagecom.example;importorg.junit.jupiter.api.Test;importstaticorg.junit.jupiter.api.Assertions.*;publicclassCalculatorTest{@TestpublicvoidtestAdd(){Calculator calc =newCalculator();assertEquals(5, calc.add(2,3));}@TestpublicvoidtestSubtract(){Calculator calc =newCalculator();assertEquals(1, calc.subtract(3,2));}@TestpublicvoidtestMultiply(){Calculator calc =newCalculator();assertEquals(6, calc.multiply(2,3));}@TestpublicvoidtestDivideNormal(){Calculator calc =newCalculator();assertEquals(2.0, calc.divide(6,3),0.001);}@TestpublicvoidtestDivideByZero(){Calculator calc =newCalculator();assertThrows(IllegalArgumentException.class,()-> calc.divide(6,0));}}5. 运行测试并生成报告
在项目根目录下运行以下命令:
mvn clean test或者,如果你想在构建过程中生成报告(例如在 prepare-package 阶段):
mvn clean verify 注意:如果使用的是 mvn verify,请确保你的 pom.xml 中 report 执行阶段设置为 prepare-package 或 verify。上面的配置是 prepare-package。
执行完成后,JaCoCo 会在 target/jacoco.exec 文件中生成覆盖率数据。报告会生成在 target/site/jacoco 目录下,其中包含 index.html 文件,这是主报告页面。
6. Mermaid 图表示例:基础配置流程
Start Maven Build
Compile Source Code
Prepare JaCoCo Agent
Run Unit Tests
Collect Coverage Data
Generate Jacoco Report
Output to target/site/jacoco
View index.html
高级配置选项 🧩
基础配置足以满足大多数需求,但 JaCoCo 提供了许多高级配置选项,以满足更复杂的需求。
1. 自定义报告路径
你可以指定覆盖率报告生成的具体位置。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals><configuration><!-- 自定义报告路径 --><outputDirectory>target/my-custom-jacoco-report</outputDirectory></configuration></execution></executions></plugin>2. 配置覆盖规则和检查
你可以设置最低覆盖率要求,如果达不到则构建失败。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals></execution><!-- 添加检查阶段 --><execution><id>check</id><phase>verify</phase><goals><goal>check</goal></goals><configuration><rules><!-- 定义规则 --><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.75</minimum><!-- 75% --></limit></limits></rule></rules></configuration></execution></executions></plugin>说明:
<phase>verify</phase>:将check目标绑定到verify生命周期阶段。<rules>:定义覆盖率检查规则。<element>BUNDLE</element>:规则作用于整个项目(Bundle)。<counter>LINE</counter>和<counter>BRANCH</counter>:指定检查的指标类型。<value>COVEREDRATIO</value>:检查覆盖率比例。<minimum>0.80</minimum>:最低覆盖率要求。
3. 排除特定类或包
有时你可能希望排除某些类(如测试类、生成代码)不参与覆盖率计算。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals><configuration><excludes><!-- 排除特定类 --><exclude>com/example/GeneratedClass.class</exclude><!-- 排除整个包 --><exclude>com/example/generated/**</exclude><!-- 排除带有特定注解的类 --><exclude>**/*Test.class</exclude></excludes></configuration></execution></executions></plugin>4. 与集成测试结合
如果你有集成测试,可以单独配置它们的覆盖率。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><!-- 为单元测试准备代理 --><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><!-- 为集成测试准备代理 --><execution><id>prepare-agent-integration</id><phase>pre-integration-test</phase><goals><goal>prepare-agent-integration</goal></goals><configuration><destFile>target/jacoco-it.exec</destFile><!-- 指定不同的输出文件 --><propertyName>jacoco-it-agent</propertyName><!-- 指定属性名 --></configuration></execution><!-- 生成单元测试报告 --><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals></execution><!-- 生成集成测试报告 --><execution><id>report-integration</id><phase>post-integration-test</phase><goals><goal>report</goal></goals><configuration><dataFile>target/jacoco-it.exec</dataFile><!-- 使用集成测试数据文件 --><outputDirectory>target/jacoco-it-report</outputDirectory><!-- 指定报告输出目录 --></configuration></execution></executions></plugin>说明:
prepare-agent-integration:为集成测试准备代理。destFile:指定集成测试覆盖率数据文件。propertyName:指定用于传递代理参数的属性名。report-integration:生成集成测试报告。
5. Mermaid 图表示例:高级配置流程
No
Yes
Start Maven Build
Compile Source Code
Prepare JaCoCo Agent
Run Unit Tests
Collect Unit Coverage Data
Run Integration Tests
Collect Integration Coverage Data
Generate Unit Report
Generate Integration Report
Check Coverage Rules
Rules Met?
Build Fail
Build Success
End
多模块项目配置 🧱
对于大型项目,通常采用多模块 Maven 结构。在这种情况下,需要在父模块或子模块中配置 JaCoCo。
1. 父模块配置
在父模块的 pom.xml 中定义 JaCoCo 插件,供所有子模块继承。
<!-- 父模块 pom.xml --><project><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>parent-project</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><modules><module>module-a</module><module>module-b</module></modules><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target><jacoco.version>0.8.12</jacoco.version></properties><build><pluginManagement><plugins><!-- JaCoCo 插件定义 --><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>${jacoco.version}</version></plugin></plugins></pluginManagement></build><!-- 定义 JaCoCo 插件的通用配置 --><profiles><profile><id>coverage</id><activation><activeByDefault>false</activeByDefault></activation><build><plugins><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals></execution><execution><id>check</id><phase>verify</phase><goals><goal>check</goal></goals><configuration><rules><rule><element>BUNDLE</element><limits><limit><counter>LINE</counter><value>COVEREDRATIO</value><minimum>0.80</minimum></limit><limit><counter>BRANCH</counter><value>COVEREDRATIO</value><minimum>0.75</minimum></limit></limits></rule></rules></configuration></execution></executions></plugin></plugins></build></profile></profiles></project>2. 子模块配置
子模块不需要重新定义插件,只需要激活 profile 即可。
<!-- module-a/pom.xml --><project><modelVersion>4.0.0</modelVersion><parent><groupId>com.example</groupId><artifactId>parent-project</artifactId><version>1.0.0-SNAPSHOT</version></parent><artifactId>module-a</artifactId><build><plugins><!-- 子模块特有的插件配置 --></plugins></build></project>3. 聚合报告
为了获得整个项目的覆盖率报告,需要在父模块中添加聚合报告的执行。
<!-- 父模块 pom.xml 的 profile 中添加 --><execution><id>report-aggregate</id><phase>verify</phase><goals><goal>report-aggregate</goal></goals><configuration><dataFile>target/jacoco.exec</dataFile><!-- 如果需要合并所有子模块数据 --><outputDirectory>target/jacoco-aggregate-report</outputDirectory></configuration></execution>注意:聚合报告需要确保子模块的 prepare-agent 阶段正确生成了数据文件。
4. Mermaid 图表示例:多模块配置流程
No
Yes
Start Multi-module Build
Parent POM Activate Coverage Profile
Apply JaCoCo Plugin to All Modules
Compile Sub-modules
Prepare Agents in Sub-modules
Run Tests in Sub-modules
Collect Coverage Data from Sub-modules
Aggregate Data
Generate Aggregate Report
Check Aggregate Coverage
Coverage OK?
Build Fail
Build Success
End
生成不同格式的报告 📊
JaCoCo 支持生成多种格式的报告,以满足不同的需求。
1. HTML 报告(默认)
HTML 报告是最常用的,它提供了直观的可视化界面,可以清晰地看到哪些代码被覆盖了,哪些没有。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals></execution></executions></plugin>2. XML 报告
XML 报告更适合于自动化工具或集成到 CI/CD 系统中。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals><configuration><formats><format>XML</format><!-- 指定格式 --></formats></configuration></execution></executions></plugin>3. CSV 报告
CSV 格式便于导入到电子表格或进行进一步的数据分析。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals><configuration><formats><format>CSV</format><!-- 指定格式 --></formats></configuration></execution></executions></plugin>4. 多格式报告
你可以同时生成多种格式的报告。
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.12</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals><configuration><formats><format>HTML</format><format>XML</format><format>CSV</format></formats></configuration></execution></executions></plugin>5. Mermaid 图表示例:报告生成流程
HTML
XML
CSV
Run Tests
Collect Coverage Data
Generate Reports
Format Required
Create HTML Report
Create XML Report
Create CSV Report
View HTML Report
Analyze Coverage
与 CI/CD 集成 🔄
将 JaCoCo 集成到 CI/CD 流水线中,可以实现自动化覆盖率检查,确保每次提交都能得到质量反馈。
1. Jenkins 示例
在 Jenkins 的 Pipeline 脚本中,你可以这样集成:
pipeline { agent any stages {stage('Checkout'){ steps { git 'https://github.com/your-repo/your-project.git'}}stage('Build and Test'){ steps { sh 'mvn clean test verify'// 执行测试和检查} post { always {// 上传覆盖率报告到 Jenkins publishCoverage adapters:[jacocoAdapter(adapterPath:'target/jacoco.exec')], failOnError:true, sourceDirectories:['src/main/java']}}}}}注意:这需要 Jenkins 中安装相应的插件(如 JaCoCo Plugin)。
2. GitHub Actions 示例
在 .github/workflows/ci.yml 文件中:
name: CI on:push:branches:[ main ]pull_request:branches:[ main ]jobs:build:runs-on: ubuntu-latest steps:-uses: actions/checkout@v4 -name: Set up JDK uses: actions/setup-java@v4 with:java-version:'11'distribution:'temurin'-name: Build with Maven run: mvn clean test verify # 可选:上传覆盖率报告到 Coveralls 或 Codecov# - name: Upload coverage to Coveralls# uses: coverallsapp/github-action@master# with:# github-token: ${{ secrets.GITHUB_TOKEN }}# path-to-lcov: target/jacoco.exec # 注意:通常需要转换格式-name: Archive coverage report uses: actions/upload-artifact@v4 with:name: jacoco-report path: target/site/jacoco/ 3. GitLab CI 示例
在 .gitlab-ci.yml 文件中:
stages:- build - test - report variables:MAVEN_OPTS:"-Xmx2g -Xms1g"before_script:- export MAVEN_HOME=/usr/share/maven - export PATH=$MAVEN_HOME/bin:$PATH build_job:stage: build script:- mvn clean compile artifacts:paths:- target/classes/ test_job:stage: test script:- mvn test artifacts:reports:junit: target/surefire-reports/*.xml# JUnit 报告paths:- target/jacoco.exec # JaCoCo 数据文件- target/site/jacoco/ # JaCoCo HTML 报告# 可选:使用 GitLab Pages 发布报告pages:stage: report script:- mvn clean test verify - cp -r target/site/jacoco/* public/ artifacts:paths:- public only:- main 4. Mermaid 图表示例:CI/CD 集成流程
Fail
Pass
Commit Code
Trigger CI Pipeline
Checkout Repository
Build Project
Run Tests
Collect Coverage Data
Generate Reports
Check Coverage Rules
Fail Build & Notify
Upload Reports
Store Artifacts
End
Deploy or Publish
最佳实践 🎯
遵循良好的实践可以帮助你更有效地使用 JaCoCo。
1. 设置合理的覆盖率阈值
不要盲目追求 100% 的覆盖率,而是根据项目重要性和业务逻辑设置合理的阈值。例如,核心业务逻辑可能需要更高的覆盖率(如 90%),而辅助功能可以稍低。
2. 结合测试类型
确保你的测试用例覆盖了不同类型的场景,包括正常流程、边界条件和异常处理。
3. 定期审查覆盖率报告
定期查看覆盖率报告,识别覆盖率低的模块或代码段,并针对性地增加测试。
4. 使用排除规则
对于一些无法或难以测试的代码(如 getter/setter、工具类等),可以使用排除规则,避免影响整体覆盖率。
5. 将覆盖率检查纳入构建流程
在 verify 阶段强制执行覆盖率检查,确保只有达到要求的代码才能被合并。
6. 集成到代码审查流程
将覆盖率报告作为代码审查的一部分,确保新提交的代码有足够的测试覆盖。
7. Mermaid 图表示例:最佳实践流程
No
Yes
Define Project Goals
Set Reasonable Coverage Thresholds
Write Comprehensive Tests
Run Tests with JaCoCo
Analyze Coverage Reports
Coverage Adequate?
Identify Gaps
Enhance Tests
Review and Approve
Update Documentation
Archive Reports
Repeat Process
实战案例:完整项目配置 🧪
让我们通过一个完整的示例来展示如何在实际项目中配置 JaCoCo。
1. 项目结构
假设我们有一个名为 my-web-app 的项目,包含以下结构:
my-web-app/ ├── pom.xml ├── src/ │ ├── main/ │ │ └── java/ │ │ └── com/example/webapp/ │ │ ├── CalculatorService.java │ │ ├── CalculatorController.java │ │ └── model/ │ │ └── CalculationResult.java │ └── test/ │ └── java/ │ └── com/example/webapp/ │ ├── CalculatorServiceTest.java │ ├── CalculatorControllerTest.java │ └── integration/ │ └── CalculatorIntegrationTest.java └── target/ 2. pom.xml 配置
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>my-web-app</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target><jacoco.version>0.8.12</jacoco.version><junit.version>5.10.2</junit.version></properties><dependencies><!-- 示例依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.18</version><!-- 使用兼容版本 --></dependency><!-- 测试依赖 --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>${junit.version}</version><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.7.18</version><scope>test</scope></dependency></dependencies><build><plugins><!-- Spring Boot Maven 插件 --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.7.18</version></plugin><!-- JaCoCo Maven Plugin --><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>${jacoco.version}</version><executions><!-- 准备代理 --><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><!-- 为集成测试准备代理 --><execution><id>prepare-agent-integration</id><phase>pre-integration-test</phase><goals><goal>prepare-agent-integration</goal></goals><configuration><destFile>target/jacoco-it.exec</destFile><propertyName>jacoco-it-agent</propertyName></configuration></execution><!-- 生成单元测试报告 --><execution><id>report</id><phase>prepare-package</phase><goals><goal>report</goal></goals></execution><!-- 生成集成测试报告 --><execution><id>report-integration</id><phase>post-integration-test</phase><goals><goal>report</goal></goals><configuration><dataFile>target/jacoco-it.exec</dataFile><outputDirectory>target/jacoco-it-report</outputDirectory></configuration></execution><!-- 检查覆盖率 --><execution><id>check</id><phase>verify</phase><goals><goal>check</goal></goals><configuration><rules><rule><element>BUNDLE</element><limits><limit><counter>LINE</counter><value>COVEREDRATIO</value><minimum>0.85</minimum><!-- 85% --></limit><limit><counter>BRANCH</counter><value>COVEREDRATIO</value><minimum>0.80</minimum><!-- 80% --></limit></limits></rule></rules></configuration></execution></executions></plugin></plugins></build><profiles><profile><id>integration-tests</id><build><plugins><!-- 配置集成测试插件 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-failsafe-plugin</artifactId><version>3.2.5</version><executions><execution><goals><goal>integration-test</goal><goal>verify</goal></goals></execution></executions></plugin></plugins></build></profile></profiles></project>3. 示例代码
CalculatorService.java
packagecom.example.webapp;importorg.springframework.stereotype.Service;@ServicepublicclassCalculatorService{publicintadd(int a,int b){return a + b;}publicintsubtract(int a,int b){return a - b;}publicintmultiply(int a,int b){return a * b;}publicdoubledivide(int a,int b){if(b ==0){thrownewIllegalArgumentException("Division by zero is not allowed.");}return(double) a / b;}// 一个未被测试的方法 (用于展示覆盖率)publicbooleanisEven(int number){return number %2==0;}}CalculatorController.java
packagecom.example.webapp;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;@RestController@RequestMapping("/calculator")publicclassCalculatorController{@AutowiredprivateCalculatorService calculatorService;@GetMapping("/add/{a}/{b}")publicResponseEntity<Integer>add(@PathVariableint a,@PathVariableint b){int result = calculatorService.add(a, b);returnResponseEntity.ok(result);}@GetMapping("/subtract/{a}/{b}")publicResponseEntity<Integer>subtract(@PathVariableint a,@PathVariableint b){int result = calculatorService.subtract(a, b);returnResponseEntity.ok(result);}@PostMapping("/multiply")publicResponseEntity<Integer>multiply(@RequestBodyCalculationRequest request){int result = calculatorService.multiply(request.getA(), request.getB());returnResponseEntity.ok(result);}@GetMapping("/divide/{a}/{b}")publicResponseEntity<Double>divide(@PathVariableint a,@PathVariableint b){try{double result = calculatorService.divide(a, b);returnResponseEntity.ok(result);}catch(IllegalArgumentException e){returnResponseEntity.badRequest().build();// 处理错误}}}// 用于 POST 请求的 DTOclassCalculationRequest{privateint a;privateint b;// Getters and SetterspublicintgetA(){return a;}publicvoidsetA(int a){this.a = a;}publicintgetB(){return b;}publicvoidsetB(int b){this.b = b;}}CalculatorServiceTest.java
packagecom.example.webapp;importorg.junit.jupiter.api.Test;importorg.springframework.boot.test.context.SpringBootTest;importstaticorg.junit.jupiter.api.Assertions.*;@SpringBootTestclassCalculatorServiceTest{privatefinalCalculatorService calculatorService =newCalculatorService();@TestvoidtestAdd(){assertEquals(5, calculatorService.add(2,3));}@TestvoidtestSubtract(){assertEquals(1, calculatorService.subtract(3,2));}@TestvoidtestMultiply(){assertEquals(6, calculatorService.multiply(2,3));}@TestvoidtestDivideNormal(){assertEquals(2.0, calculatorService.divide(6,3),0.001);}@TestvoidtestDivideByZero(){assertThrows(IllegalArgumentException.class,()-> calculatorService.divide(6,0));}// 为未测试的方法添加测试 (可选)@TestvoidtestIsEvenTrue(){assertTrue(calculatorService.isEven(4));}@TestvoidtestIsEvenFalse(){assertFalse(calculatorService.isEven(3));}}CalculatorControllerTest.java
packagecom.example.webapp;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;importorg.springframework.boot.test.mock.mockito.MockBean;importorg.springframework.http.MediaType;importorg.springframework.test.web.servlet.MockMvc;importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@WebMvcTest(CalculatorController.class)classCalculatorControllerTest{@AutowiredprivateMockMvc mockMvc;@MockBeanprivateCalculatorService calculatorService;@TestvoidtestAdd()throwsException{ mockMvc.perform(get("/calculator/add/2/3")).andExpect(status().isOk()).andExpect(content().string("5"));}@TestvoidtestSubtract()throwsException{ mockMvc.perform(get("/calculator/subtract/5/3")).andExpect(status().isOk()).andExpect(content().string("2"));}@TestvoidtestMultiply()throwsException{String requestBody ="{\"a\": 3, \"b\": 4}"; mockMvc.perform(post("/calculator/multiply").contentType(MediaType.APPLICATION_JSON).content(requestBody)).andExpect(status().isOk()).andExpect(content().string("12"));}@TestvoidtestDivideNormal()throwsException{ mockMvc.perform(get("/calculator/divide/8/2")).andExpect(status().isOk()).andExpect(content().string("4.0"));}@TestvoidtestDivideByZero()throwsException{ mockMvc.perform(get("/calculator/divide/8/0")).andExpect(status().isBadRequest());// 模拟控制器处理了异常}}CalculatorIntegrationTest.java
packagecom.example.webapp.integration;importcom.example.webapp.CalculatorController;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.web.client.TestRestTemplate;importorg.springframework.http.ResponseEntity;importstaticorg.assertj.core.api.Assertions.assertThat;@SpringBootTest(webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)classCalculatorIntegrationTest{@AutowiredprivateTestRestTemplate restTemplate;@TestvoidtestAddViaRest(){ResponseEntity<Integer> response = restTemplate.getForEntity("/calculator/add/2/3",Integer.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);assertThat(response.getBody()).isEqualTo(5);}@TestvoidtestSubtractViaRest(){ResponseEntity<Integer> response = restTemplate.getForEntity("/calculator/subtract/5/3",Integer.class);assertThat(response.getStatusCodeValue()).isEqualTo(200);assertThat(response.getBody()).isEqualTo(2);}}4. 运行命令
# 1. 运行单元测试和生成报告 (包括检查) mvn clean test verify # 2. 运行集成测试 (需要先激活 profile) mvn clean test verify -Pintegration-tests # 3. 仅生成报告 (不执行测试) mvn jacoco:report 5. Mermaid 图表示例:完整项目流程
No
Yes
Project Structure
Configure pom.xml
Define Dependencies
Add JaCoCo Plugin
Write Unit Tests
Write Integration Tests
Run mvn clean test verify
Prepare Agent
Run Unit Tests
Collect Unit Coverage
Run Integration Tests
Collect Integration Coverage
Generate Reports
Check Coverage Rules
Rules Met?
Build Fail
Build Success
End
View Reports
Analyze and Improve
常见问题与解决方案 ❗
1. 为什么覆盖率报告为空?
原因:
- 测试未执行。
- 未正确配置 JaCoCo 插件。
- 使用了
@RunWith等旧的 JUnit 注解(Junit 4)导致代理未生效。
解决方案:
- 确保运行了
mvn test或mvn verify。 - 检查
pom.xml中的插件配置是否正确。 - 如果使用 JUnit 4,请确保
pom.xml中包含了junit-jupiter-engine或升级到 JUnit 5。
2. 如何排除某些包或类?
解决方案:使用 <excludes> 配置。
<configuration><excludes><exclude>com/example/config/**</exclude><exclude>com/example/generated/**</exclude><exclude>**/*Test.class</exclude></excludes></configuration>3. 如何在 CI/CD 中获取覆盖率报告?
解决方案:
- 确保构建命令包含
mvn clean test verify。 - 将
target/site/jacoco/目录作为 artifact 上传。 - 使用 CI/CD 工具提供的报告功能(如 Jenkins 的 JaCoCo 插件)。
4. 为什么覆盖率检查失败?
原因:
- 达不到设定的阈值。
- 配置错误。
解决方案:
- 检查报告中的具体指标。
- 调整阈值或增加测试用例。
- 确认
check执行阶段正确绑定。
总结 🎯
JaCoCo 是一个功能强大且易于使用的 Java 代码覆盖率工具。通过将其与 Maven 集成,我们可以轻松地自动化地进行覆盖率分析,生成详细的报告,并将其融入到我们的 CI/CD 流程中。从基础配置到高级选项,再到多模块项目和 CI/CD 集成,JaCoCo 都能提供强有力的支持。
通过本文的学习,你应该掌握了如何在 Maven 项目中配置 JaCoCo 插件,生成不同格式的报告,设置覆盖率检查规则,以及如何在 CI/CD 环境中利用它。记住,代码覆盖率只是质量保障的一部分,但它是一个非常有用的指标,可以帮助我们构建更健壮、更可靠的软件。持续关注覆盖率报告,并根据报告优化测试用例,是提升代码质量的重要途径。🚀
附录:相关链接 📚
希望这篇博客能帮助你深入了解并掌握 JaCoCo Maven 插件的强大功能!
🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨