How to fix a drop of code coverage with a multi module Maven project
Adding code coverage to SonarQube is quite easy for any Maven project nowadays. Just add the jacoco-maven-plugin dependency to your pom.xml, add the prepare-agent
execution task, and you are good to go. Even for multiple modules this works out of the box. But time goes on and your application grows as well. You start moving code to other modules, and somehow SonarQube no longer seems to pick up the covered code. What the heck is going on?
The answer is shamefully simple. You did not write proper unit tests, but component tests. JaCoCo assumes at default every module has its own set of tests. So, as soon as your test covers multiple modules, only the 'current' module is counted as covered code.
To make this a little clearer, consider following silly example[1]. Let’s say we have two modules, a 'Utils' module and an 'Animals' module. The 'Utils' module contains the following class:
@UtilityClass
public class class AnimalUtils {
public static int discoverLegs(Animal animal) {
return animal instanceof Arachnid ? 8 : 4;
}
}
In the 'Animals' module, a spider implementation of an animal can be found:
@Getter
public class Spider extends Arachnid {
private int legs;
public void setLegs() {
this.legs = AnimalUtils.discoverLegs(this);
}
}
And in the same 'Animals' module, there is a test to prove a spider has eight legs:
@Test
private void aSpiderHasEightLegs() {
Spider aragog = new Spider();
assertEquals(8, aragog.getLegs());
}
The test works like a charm; you can even stop at a breakpoint in the AnimalUtils discoverLegs
method when running the aSpiderHasEightLegs
in debug mode. And yet SonarQube does not list the discoverLegs
method as being covered with a test! Why? It is because the AnimalUtils class does not have a test in the 'Utils' module. As JaCoCo does not report across modules, SonarQube simply does not know the discoverLegs
method is covered.
If you look back at the example, theoretically it should be of no concern to the Spider class how the AnimalUtils class discovers spiders do have 8 legs. So if you want to fix this problem the proper way, the test should be split up to two tests. One for the discovery of animal legs[2], the other to prove the Spider class uses AnimalUtils to get the amount of legs. But we don’t live in a perfect world, so we just want to tell to JaCoCo above situation is good enough as well.
To do this, we need to configure JaCoCO with an extra task to aggregate all executed tests. A good practice is to add the plugin with all the executions to your parent pom in the pluginManagement section.
<pluginManagement>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<executions>
<execution>
<id>prepare-agent</id>
<phase>initialize</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<goals>
<goal>report-aggregate</goal>
</goals>
<phase>verify</phase>
</execution>
</executions>
</plugin>
</pluginManagement>
In every module where you need unit tests, you just add the plugin:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
Now, when maven verify
is ran, a jacoco.xml file is created within each modules /target/site/jacoco-aggregate
folder. Every jacoco.xml file does describe the lines of code covered by its modules tests. The lines of code can either be of the module itself or be any other module. To look back at above example, the jacoco.xml file of the 'Animals' module describes both coverage for the Spider class and the AnimalUtils class.
There is one last thing to do. SonarQube does not yet pick up the xml reports automatically, so we need to tell SonarQube manually[3]. This can be done easily by adding the sonar.coverage.jacoco.xmlReportPaths
property.
For our example, that would be:
mvn clean verify sonar:sonar -Dsonar.coverage.jacoco.xmlReportPaths=${WORKSPACE}/utils/target/site/jacoco-aggregate/jacoco.xml,${WORKSPACE}/animals/target/site/jacoco-aggregate/jacoco.xml