理解 JUnit, JaCoCo 到 SonarQube 的过程及 Maven 配置

Java 项目需要产生单元测试及代码覆盖率的话一直都是走的 JUnit 单元测试,JaCoCo 基于测试产生测试覆盖率,然后送到 SonarQube 去展示这条路子。当然 SonarQube 还可以帮我们进行代码的静态分析。但对其中的具体使用及过程知晓的并不深,基本就是在 pom.xml 中依葫芦画瓢。本文稍加深入的理解每一步的功效与配置,以 Maven 管理的 Java 项目为例,JUnit 采用是众多旧项目仍然无法摆脱的 JUnit 4。

示例项目名称为 JaCoCoSonar, 创建一个 Calc 类,其中有 int add(int op1, int op2) 方法,为其写一个单元测试 CalcTest

单元测试实际是被 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-pluginorg.codehaus.mojo:maven-xxx-plugin,并执行插件  maven-xxx-pluginfoo 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 中加上

现在可以简单的执行命令了,如

mvn jacoco:help

它显示的 JaCoCo Maven 插件的使用帮助与前面执行的那两命令显示的结果是一致的

mvn jacoco:help 列举出来的 Maven goal 有

  1. jacoco:check
  2. jacoco:dump
  3. jacoco:help
  4. jacoco:instrument
  5. jacoco:merge
  6. jacoco:prepare-agent
  7. jacoco:prepare-agent-integration
  8. jacoco:report
  9. jacoco:report-aggregate
  10. jacoco:report-integration
  11. 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

我们查看它的源代码 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 参数

执行单元测试时有如下输出

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

此时就会在控制台中看到非常冗余的信息,搜索可以找到

这些就是当前所用的默认参数,我们可对 jacoco:prepare-agent 进行定制,如 jacoco.exec 数据文件的路径,或在 <configuration> 中配置,或定义成 Maven 的属性值。

实际项目中看过不少类似于如下方式的配置

如果 surefire 插件没有额外参数的话就保持用默认的 argLine 就行, 而要是对 surefire 配置了 <argLine>,只需在 ${argLine} 基础之上附加别的内容, 例如在升级 JDK 到版本 17 后可能需要修改 surefire 插件的 <argLine> 属性

或许这时候在 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 coverageSonarScanner 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 时需经历的以下几个步骤

  1. mvn jacoco:prepare-agent: 为 surefire 插件准备好  JaCoCo agent 参数
  2. mvn test: surefire 插件应用 JaCoCo agent 运行单元测试,生成  target/jacoco.exec
  3. mvn jacoco:report, 在 target 下生成  site/jacoco/jacoco.xml 文件
  4. 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

注:

  1. 如果 sonar.host.url 是 http://localhost:9000, 可省略该属性
  2. Sonar 配置了 user token 的话, -Dsonar.login=admin -Dsonar.password=password 可被替换为 -Dsonar.login=<user-token>
  3. 因此,mvn 命令可相应的变为

    mvn clean jacoco:prepare-agent test jacoco:report sonar:sonar -Dsonar.login=sqp_528352b3f2eeb0189b0e853ddb0f6d9a005237df

SonarQube 分析完后,我们就可以看到

 

如果我们在 pom.xml 中对 jacoco 插件进行一些配置

现在要把代码覆盖率送到 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%,可进行如下排查

  1. 在 target 目录中是否生成了  jacoco.exec 和 site/jacoco/jacoco.xml 文件
  2. mvn 执行过程中是否执行了 jacoco:prepare-agent, jacoco:report 任务 -- 检查 mvn 控制台
  3. mvn -X 开启详尽的执行过程信息,从中能看到实际的 jacoco:prepare-agent, jacoco:report 具体的配置
  4. 如果 sonar:sonar 执行过程中出现 OutOfMemoryError, 请为 Maven 配置更多的堆内存,而不是默认物理内存的 1/4。配置环境变量 MAVEN_OPTS="-Xmx512M"

参考:

  1. 使用 JaCoCo Maven插件创建代码覆盖率报告
  2. Intro to JaCoCo

本文链接 https://yanbin.blog/junit-jacoco-sonarqube-maven/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments