多模块Spring Boot项目Jacoco-Sonar集成:Controller覆盖率未统计问题排查
一、让Controller被Jacoco覆盖率统计的具体方案
你遇到的Controller未被集成测试(用MockMvc调用)统计覆盖率的问题,核心原因通常是Jacoco Agent没有在集成测试阶段正确拦截Spring MVC的请求调用链,或者单元测试与集成测试的覆盖率数据没被正确合并。结合你这种Spring Boot多模块的场景,按下面的步骤调整配置就能解决:
1. 确保集成测试阶段启动Jacoco Agent
在根pom.xml的<build>节点下配置jacoco-maven-plugin,为单元测试和集成测试分别配置Agent,收集各自的覆盖率数据:
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <!-- 收集单元测试覆盖率 --> <execution> <id>jacoco-ut</id> <goals> <goal>prepare-agent</goal> </goals> <configuration> <destFile>${project.build.directory}/jacoco-ut.exec</destFile> <propertyName>jacoco.ut.agent.argLine</propertyName> </configuration> </execution> <!-- 收集集成测试覆盖率 --> <execution> <id>jacoco-it</id> <goals> <goal>prepare-agent-integration</goal> </goals> <configuration> <destFile>${project.build.directory}/jacoco-it.exec</destFile> <propertyName>jacoco.it.agent.argLine</propertyName> </configuration> </execution> <!-- 生成单元测试报告 --> <execution> <id>jacoco-ut-report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> <configuration> <dataFile>${project.build.directory}/jacoco-ut.exec</dataFile> <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory> </configuration> </execution> <!-- 生成集成测试报告 --> <execution> <id>jacoco-it-report</id> <phase>post-integration-test</phase> <goals> <goal>report</goal> </goals> <configuration> <dataFile>${project.build.directory}/jacoco-it.exec</dataFile> <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory> </configuration> </execution> </executions> </plugin>
同时,要确保集成测试使用的maven-failsafe-plugin能用到Jacoco的Agent参数,不然集成测试的调用还是不会被记录:
<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> <configuration> <argLine>${jacoco.it.agent.argLine}</argLine> </configuration> </plugin>
这样配置后,MockMvc发起的请求会通过Spring的DispatcherServlet转发到Controller实例,Jacoco Agent会记录下这些调用轨迹,Controller的覆盖率就会被统计到了。
2. 合并单元测试与集成测试的覆盖率数据
多模块场景下,需要把各模块的单元测试(jacoco-ut.exec)和集成测试(jacoco-it.exec)数据合并,才能在Sonar中看到完整的覆盖率。你可以创建一个单独的聚合模块(后面解释为什么不推荐在根模块做),在这个模块的pom.xml中配置:
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <id>jacoco-aggregate</id> <phase>verify</phase> <goals> <goal>report-aggregate</goal> </goals> <configuration> <dataFiles> <!-- 替换成你的web模块路径 --> <dataFile>${project.parent.basedir}/web/target/jacoco-ut.exec</dataFile> <dataFile>${project.parent.basedir}/web/target/jacoco-it.exec</dataFile> <!-- 替换成你的业务模块路径 --> <dataFile>${project.parent.basedir}/service/target/jacoco-ut.exec</dataFile> <!-- 其他模块的exec文件都加在这里 --> </dataFiles> <outputDirectory>${project.reporting.outputDirectory}/jacoco-aggregate</outputDirectory> </configuration> </execution> </executions> </plugin>
最后在Sonar的配置中指定这个聚合报告的路径,让Sonar读取合并后的完整覆盖率数据:
<plugin> <groupId>org.sonarsource.scanner.maven</groupId> <artifactId>sonar-maven-plugin</artifactId> <version>3.10.0.2594</version> <configuration> <sonar.jacoco.reportPaths>${project.parent.basedir}/aggregate/target/site/jacoco-aggregate/jacoco.xml</sonar.jacoco.reportPaths> </configuration> </plugin>
二、解答你的两个疑问
疑问1:为何必须单独创建模块来聚合结果,不能在根模块完成?
其实不是绝对必须,但在根模块做聚合会踩Maven生命周期的坑:
- 根模块是
pom类型,它的生命周期阶段(比如verify)不会自动等待所有子模块的test和integration-test阶段执行完毕。如果在根模块配置report-aggregate,很可能子模块的exec文件还没生成,根模块就已经执行聚合操作了,导致报告为空。 - 单独的聚合模块(同样是
pom类型)可以通过依赖所有业务子模块,确保子模块的所有测试任务先执行完,再进行覆盖率数据的聚合,避免时序问题。 - 从维护角度看,单独的聚合模块把覆盖率聚合逻辑和业务代码解耦,后续调整起来更清晰,不会和业务模块的配置混在一起。
疑问2:Spring Boot项目用MockMvc做集成测试,Controller不是直接实例化调用,为什么会影响覆盖率?
MockMvc的工作逻辑是模拟Servlet容器环境,通过Spring的DispatcherServlet转发请求到容器管理的Controller Bean——这个Controller实例是Spring正常初始化的,Jacoco Agent会在类加载时给Controller的字节码织入统计逻辑,只要DispatcherServlet调用了Controller的方法,Jacoco就会记录下这些调用。
之前没统计到覆盖率,不是MockMvc的问题,而是:
- 集成测试阶段没有启动Jacoco Agent,导致Controller的调用轨迹没被记录;
- 集成测试生成的
exec文件没被合并到最终的覆盖率报告中,Sonar没读到这部分数据。
内容的提问来源于stack exchange,提问作者slim

