Java 项目需要产生单元测试及代码覆盖率的话一直都是走的 JUnit 单元测试,JaCoCo 基于测试产生测试覆盖率,然后送到 SonarQube 去展示这条路子。当然 SonarQube 还可以帮我们进行代码的静态分析。但对其中的具体使用及过程知晓的并不深,基本就是在 pom.xml 中依葫芦画瓢。本文稍加深入的理解每一步的功效与配置,以 Maven 管理的 Java 项目为例,JUnit 采用是众多旧项目仍然无法摆脱的 JUnit 4。
示例项目名称为 JaCoCoSonar, 创建一个 Calc 类,其中有 int add(int op1, int op2) 方法,为其写一个单元测试 CalcTest
1 2 3 4 5 6 7 |
public class CalcTest { @Test public void testAdd() { Assert.assertEquals(3, Calc.add(1, 2)); } } |
单元测试实际是被 maven-surefire-plugin 插件执行的
现在开始第一步,执行 mvn test
看会发生什么,执行过程中控制台显示
[INFO] --- surefire:3.1.2:test (default-test) @ JaCoCoSonar ---
[INFO] Using auto detected provider org.apache.maven.surefire.junit4.JUnit4Provider
说明 Maven 使用 surefire 插件来运行单元测试(以上 JaCoCoSnoar 是本项目的名称),并且选择的 Provider 是 JUnit4Provider。倘若项目中同时引入了 JUnit 4 和 JUnit 5 依赖,mvn test
无法发现 JUnit 4 的单元测试时需注意 surefire 插件是用的哪个 Provider。
注:在 pom.xml 可能根本没有配置 maven-surefire-plugin 插件啊,可以 mvn test
就是知道用哪个插件,这是 Maven 内部事件。Maven 的约定是官方插件命名为 maven-xxx-plugin, 而第三方插件用 xxx-maven-plugin,当执行 mvn xxx:foo
命令时,Maven 会试图查找 org.apache.maven.plugins:maven-xxx-plugin
或 org.codehaus.mojo:maven-xxx-plugin
,并执行插件 maven-xxx-plugin
的 foo
goal, 所以 mvn test
也可以写成 mvn surefire:test
完后回到 Maven 项目目录, mvn test
为该单元测试 CalcTest 生成了
target/surefire-reports
├── TEST-org.example.CalcTest.xml
└── org.example.CalcTest.txt
一些可视化展示单元测试报告的工具可读取 target/surefire-reports 中内容。
JaCoCo 如何产生代码覆盖率报告的
单元测试报告产生后,我们再过度到 Maven 中如何使用 JaCoCo。JaCoCo 是由 EclEmma 团队创建的生成 Java 代码覆盖率报告的工具,记得以前用过 Emma 和 JCoverage 生成过覆盖率报告,不知 Emma 与 EclEmma 与之前的 Emma 有何关系。
执行一个 Maven 第三方插件,非官方插件(插件命令格式为 maven-xxx-plugin)若不在 pom.xml 中配置的话,在执行时可直接指定插件的 groupId:artifactId:version:goal, 如
mvn org.jacoco:jacoco-maven-plugin:0.8.1:help
mvn help:describe -Dplugin=org.jacoco:jacoco-maven-plugin -Ddetail
或者定义一个环境变量, 也能达到简约的效果,如
export jacoco=org.jacoco:jacoco-maven-plugin:0.8.1
mvn ${jacoco}:help
为了避免书写冗长的 mvn 命令,或使用命令更友好,还是有必要在 pom.xml 中引入 JaCoCo 插件,在 build/plugins 中加上
1 2 3 4 5 |
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> </plugin> |
现在可以简单的执行命令了,如
mvn jacoco:help
它显示的 JaCoCo Maven 插件的使用帮助与前面执行的那两命令显示的结果是一致的
mvn jacoco:help
列举出来的 Maven goal 有
- jacoco:check
- jacoco:dump
- jacoco:help
- jacoco:instrument
- jacoco:merge
- jacoco:prepare-agent
- jacoco:prepare-agent-integration
- jacoco:report
- jacoco:report-aggregate
- jacoco:report-integration
- jacoco:restore-instrumented-classes
现在 pom.xml 中有 JaCoCo 插件后,再次执行之前的 mvn test
, 还是老样子, target 目录中并没有产生新的目录或文件
比如说现在没有 Google, 光从 jacoco:help 会想当然的尝试什么命令呢?
既然是产生代码覆盖率报告的工具,那先试下 mvn jacoco:report
:
[INFO] --- jacoco:0.8.11:report (default-cli) @ JaCoCoSonar ---
[INFO] Skipping JaCoCo execution due to missing execution data file.
提示说缺 data file. 那如何产生 JaCoCo 的 data file 呢?提前说明一下 jacoco 与 surefire 插件是如何协同工作的, surefire 运行测试时, jacoco 将以 agent 身份产生 JaCoCo 的 data file, 即 jacoco.exec. 所以再试下 mvn jacoco:prepare-agent
:
[INFO] --- jacoco:0.8.11:prepare-agent (default-cli) @ JaCoCoSonar ---
[INFO] argLine set to -javaagent:/Users/yanbin/.m2/repository/org/jacoco/org.jacoco.agent/0.8.11/org.jacoco.agent-0.8.11-runtime.jar=destfile=/Users/yanbin/Workspaces/tests/JaCoCoSonar/target/jacoco.exec
上面向我们报告的就是它会设置 argLine 为值 -javaagent....
,正好 maven-surefire-plugin 的默认 <argLine> 配置引用的就是 ${argLine},这样 JaCoCo 就完美的嵌入到了 surefire 当中的。我们可在命令行中一口气执行上两个任务
mvn jacoco:prepare-agent test
这时候在 target 目录中就会产生 jacoco.exec
文件,这是一个二进制文件,需要用 mvn jacoco:report
进一步生成友好的覆盖率测试报告。它会在 target 中生成 site/jacoco 目录
在网页中打开 target/site/jacoco/index.html 文件
点击包名 org.example 一路可查到测试方法,并能在源代码中显示覆盖的代码行。
学习到这里的话,如果想要一次性为 surefire 配置 argLine, 运行测试,生成测试报告,jacoco.exec 文件及覆盖率报告的 mvn 命令就是
mvn jacoco:prepare-agent test jacoco:report
在 pom.xml 只需要引入 jacoco 插件并连续执行多个 Maven 任务就能得到我们的所需。不过,多数人大概不希望在构建时输入太长的命令,那么自动执行的步骤可以配置到 pom.xml 的插件当中。
我们唯一要做的就是配置 jacoco 插件的 prepare-agent goal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> |
我们查看它的源代码 jacoco/jacoco-maven-plugin/src/org/jacoco/maven/AgentMojo.java 可知 prepare-agent
绑定到的 Maven 的 phase 是 LifecyclePhase.INITIALIZE. 而 report 是绑定在 LifecyclePhase.VERIFY.
现在,与 mvn jacoco:prepare-agent test jacoco:report
等效的命令就只需用 mvn verify
了,中间步骤在 INITIALIZE 阶段中自动完成, 最后产生 JaCoCo 代码覆盖率报告。
我们可以在单元测试中插入代码获取输入的 JVM 参数
1 2 3 4 5 6 7 8 |
// import java.lang.management.*; RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean(); List<String> listOfArguments = runtimeMxBean.getInputArguments(); for (String arg: listOfArguments) { System.out.println("ARG: " + arg); } |
执行单元测试时有如下输出
ARG: -javaagent:/Users/yanbin/.m2/repository/org/jacoco/org.jacoco.agent/0.8.11/org.jacoco.agent-0.8.11-runtime.jar=destfile=/Users/yanbin/Workspaces/tests/JaCoCoSonar/target/jacoco.exec
如果使用 jacoco goal 时不想使用默认配置的话,可以自定义。执行 mvn 命令时加上 -X
参数可得到非常详尽的输出信息
mvn -X test
此时就会在控制台中看到非常冗余的信息,搜索可以找到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[DEBUG] Goal: org.jacoco:jacoco-maven-plugin:0.8.11:prepare-agent (prepare-agent) [DEBUG] Style: Regular [DEBUG] Configuration: <?xml version="1.0" encoding="UTF-8"?> <configuration> <address>${jacoco.address}</address> <append>${jacoco.append}</append> <classDumpDir>${jacoco.classDumpDir}</classDumpDir> <destFile default-value="${project.build.directory}/jacoco.exec">${jacoco.destFile}</destFile> <dumpOnExit>${jacoco.dumpOnExit}</dumpOnExit> <exclClassLoaders>${jacoco.exclClassLoaders}</exclClassLoaders> <inclBootstrapClasses>${jacoco.inclBootstrapClasses}</inclBootstrapClasses> <inclNoLocationClasses>${jacoco.inclNoLocationClasses}</inclNoLocationClasses> <jmx>${jacoco.jmx}</jmx> <output>${jacoco.output}</output> <pluginArtifactMap>${plugin.artifactMap}</pluginArtifactMap> <port>${jacoco.port}</port> <project>${project}</project> <propertyName>${jacoco.propertyName}</propertyName> <sessionId>${jacoco.sessionId}</sessionId> <skip default-value="false">${jacoco.skip}</skip> </configuration> |
这些就是当前所用的默认参数,我们可对 jacoco:prepare-agent 进行定制,如 jacoco.exec 数据文件的路径,或在 <configuration> 中配置,或定义成 Maven 的属性值。
实际项目中看过不少类似于如下方式的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> <configuration> <propertyName>surefireArgLine</propertyName> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.2</version> <configuration> <argLine>${surefireArgLine}</argLine> </configuration> </plugin> </plugins> </build> |
如果 surefire 插件没有额外参数的话就保持用默认的 argLine 就行, 而要是对 surefire 配置了 <argLine>,只需在 ${argLine} 基础之上附加别的内容, 例如在升级 JDK 到版本 17 后可能需要修改 surefire 插件的 <argLine> 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.2</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit4</artifactId> <version>${surefire.plugin.version}</version> </dependency> </dependencies> <configuration> <argLine> ${argLine} --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.time.format=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED </argLine> </configuration> </plugin> |
或许这时候在 jacoco 插件中声明一个 propertyName: surefireArgLine(或声明一个 Maven 全局属性 jacoco.propertyName=surefireArgLine ), 接着在 surefire 中使用 ${surefireArgLine} 作为 <argLine> 一部分会友好些。
对于多模块的 Maven 项目,jacoco 和 surefire 插件可配置在最顶层的 parent 模块(pom type), 运行 mvn test
后,jacoco.exec
会生成在每一个叶子模块的 target 目录中。使用 mvn jacoco:report-aggregate
能够把每一个叶子模块的覆盖率报告汇集生成了主模块的 target/site/jacoco-aggregate 目录中。
JaCoCo 与 SonarQube 的集成
SonarQube 是围绕着代码质量的综合性分析,报告工具,它可详细的展示单元测试和代码覆盖率报告,以及对代码进行静态分析,支持许多的编程语言。关于 SonarQube 与 Java 项目的集成 请可参考它的官方文档 SonarQube - Java test coverage 及 SonarScanner for Maven
首先我们用 Docker 启动一个 SonarQube 容器,选择当前的 LTS 社区版镜像 sonarqube:9-community
docker run -it -p 9000:9000 sonarqube:9-community
SonarQube 容器启动可能要花一分钟,启动后浏览器中打开 http://localhost:9000/ 访问,初始用户名和密码是 admin/admin,登陆后要求修改密码,我们改为 password(和没改一样)。
在等待 SonarQube 完全就绪前先了解一下 Maven 项目, JaCoCo 集成 SonarQube 时需经历的以下几个步骤
- mvn jacoco:prepare-agent: 为 surefire 插件准备好 JaCoCo agent 参数
- mvn test: surefire 插件应用 JaCoCo agent 运行单元测试,生成 target/jacoco.exec
- mvn jacoco:report, 在 target 下生成 site/jacoco/jacoco.xml 文件
- mvn sonar:sonar 把单元测试的结果,覆盖率数据及源代码送到 SonarQube 并产生报告。它实际上调用的是插件 org.sonarsource.scanner.maven:sonar-maven-plugin 的 goal sonar
如果在 pom.xml 中没有配置 jacoco 插件的 <executions> 的话,可以执行下面一系列的 Maven task 来生成 Sonar 报告
mvn clean jacoco:prepare-agent test jacoco:report sonar:sonar -Dsonar.login=admin -Dsonar.password=password -Dsonar.host.url=http://localhost:9000
注:
- 如果 sonar.host.url 是 http://localhost:9000, 可省略该属性
- Sonar 配置了 user token 的话, -Dsonar.login=admin -Dsonar.password=password 可被替换为 -Dsonar.login=<user-token>
- 因此,mvn 命令可相应的变为
mvn clean jacoco:prepare-agent test jacoco:report sonar:sonar -Dsonar.login=sqp_528352b3f2eeb0189b0e853ddb0f6d9a005237df
SonarQube 分析完后,我们就可以看到
如果我们在 pom.xml 中对 jacoco 插件进行一些配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <goals> <goal>report</goal> </goals> <configuration> <formats> <format>XML</format> </formats> </configuration> </execution> </executions> </plugin> |
现在要把代码覆盖率送到 SonarQube 的 Maven 命令
mvn clean verify sonar:sonar -Dsonar.login=sqp_528352b3f2eeb0189b0e853ddb0f6d9a005237df
如果是多模块的 Maven 项目,须先执行 mvn install,如
mvn clean install sonar:sonar -Dsonar.login=sqp_528352b3f2eeb0189b0e853ddb0f6d9a005237df
install
会触发 verify
大体上整个 JUnit, JaCoCo 到 SonarQube 的整个过程就是这样子,剩下的就是怎么定制 jacoco 插件,比如定制 argLine 的名称,jacoco.exec 的路径,或在 pom.xml 配置名为 coverage 的 profile 才执行 JaCoCo, Sonar 任务,或在 settings.xml 中配置 sonar.host.url 和 sonar.login 属性,或用 jacoco:prepare-agent-integration, jacoco:report-integration, 搭配插件 maven-failsafe-plugin 为集成测试产生代码覆盖率报告
Maven, JaCoCo, SonarQube 问题诊断
如果在 SonarQube 上显示的代码覆盖率为 0%,可进行如下排查
- 在 target 目录中是否生成了 jacoco.exec 和 site/jacoco/jacoco.xml 文件
- mvn 执行过程中是否执行了 jacoco:prepare-agent, jacoco:report 任务 -- 检查 mvn 控制台
- mvn -X 开启详尽的执行过程信息,从中能看到实际的 jacoco:prepare-agent, jacoco:report 具体的配置
- 如果 sonar:sonar 执行过程中出现 OutOfMemoryError, 请为 Maven 配置更多的堆内存,而不是默认物理内存的 1/4。配置环境变量 MAVEN_OPTS="-Xmx512M"
参考:
本文链接 https://yanbin.blog/junit-jacoco-sonarqube-maven/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。