Personal Programming Notes

To err is human; to debug, divine.

Jacoco in Maven Projects

This blog post goes over some recipes for adding code coverage report to Maven-based projects with Jacoco.

Standard usage

Based on offical instruction and this, you need to add the following code snippet in to your Maven pom.xml.

Jacoco usage (typical Maven project)
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
29
30
31
32
33
<project>
...

    <dependencies>
        ...
    </dependencies>

    <build>
        <plugins>
            ...
            <!-- Code Coverage report generation -->
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.7.9</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>generate-code-coverage-report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

At least, you need “prepare-agent” before test phase for Jacoco instrumentation and “report” after test phase for generating the report. You could subject the project to code coverage and generate the same report without making any changes to the pom file. To do this, run the following command:

Jacoco from Maven command-line
1
mvn jacoco:prepare-agent test jacoco:report

You may get the following error:

1
[ERROR] No plugin found for prefix 'jacoco' in the current project ...

There are two options to fix that error. The easiest way is to specify the groupId and artifactId parameters of the plugin explicitly. You can also add version to ensure the stability of your build pipeline.

1
mvn clean org.jacoco:jacoco-maven-plugin:0.7.9:prepare-agent install org.jacoco:jacoco-maven-plugin:0.7.9:report

The more long-term solution is to add the following in to your Maven “settings.xml”.

Maven settings
1
2
3
<pluginGroups>
    <pluginGroup>org.jacoco</pluginGroup>
</pluginGroups>

Tests with Mock

If mocking is involved in unit tests, you need to use “instrument” and “restore-instrumented” steps.

Reference:

Multi-module Maven projects

Officially, multi-module Maven projects are supported differently by Jacoco as documented here. Instrumentation will be similar but the challenge of multi-module Maven projects lies in how to collect and report code coverage of all modules correctly. Jacoco Maven standard goals, as shown in sections above, work on single modules only: Tests are executed within the module and contributed coverage only to code within the same module. Coverage reports were created for each module separately.

In the past, there are some ad-hoc solutions such as this (for Jacoco 0.5.x) to work around that limit. However, those patterns are also error-prone and hard to customize, especially when Jacoco is used with Surefire plugin. Fortunately, Jacoco recently introduced a new Maven goal “report-aggregate” in its release 0.7.7 which will aggregate code coverage data across Maven modules. Its usage is also present in the same link (quoted below) but it is too succint and not very helpful for new users.

Create a dedicated module in your project for generation of the report. This module should depend on all or some other modules in the project.

Let' say you have a multi-module Maven project with this structure:

Multi-module Maven project
1
2
3
4
root pom
  |- module a
  |- module b
  |- module c

To use Jacoco “report-aggregate” goal for these modules, you first need to add a dedicated “coverage” module. This “coverage” module should be added into the root POM. The multi-module Maven project should now look like this:

Multi-module Maven project with aggregate coverage module
1
2
3
4
5
root pom
  |- module a
  |- module b
  |- module c
  |- module "coverage"

The POMs for each module does not need to change at all. The POM for the “coverage” module will look like this:

Maven pom.xml for coverage module
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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>

    <parent>
        <groupId>com.company</groupId>
        <artifactId>company-pom</artifactId>
        <version>3.0</version>
    </parent>

    <artifactId>report</artifactId>
    <name>Jacoco Report</name>

    <dependencies>
        <dependency>
            <groupId>my.example</groupId>
            <artifactId>module-a</artifactId>
            <version>210.0.00-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>my.example</groupId>
            <artifactId>module-b</artifactId>
            <version>210.0.00-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>my.example</groupId>
            <artifactId>module-c</artifactId>
            <version>210.0.00-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.7.9</version>
                <configuration>
                    <excludes>
                        <!-- Example of excluding classes 
                        <exclude>**/com/company/config/AutoConfiguration.class</exclude>
                        -->
                    </excludes>
                </configuration>
                <executions>
                    <execution>
                        <id>report-aggregate</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>report-aggregate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- This coverage module should never be deployed -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Note that we still require “prepare-agent” step to run before the first test suite. Depending on what plugins are being used and how the modules are organized within the project, we might have different setup for that particular step. One option is to run from the command-line:

Maven pom.xml for coverage module
1
2
3
4
5
tdongsi$ ls
Dockerfile              README.md               module1       pom.xml
Jenkinsfile             coverage                module2       scripts

tdongsi$ mvn -B clean org.jacoco:jacoco-maven-plugin:0.7.9:prepare-agent install

Links:

Customizations

In theory, a global threshold can be defined in coverage/pom.xml to enforce code coverage standard across teams. However, in practice, different teams are at different stages of module/service maturity and blindly having a global threshold will hamper teams working on newer services/modules. In addition, it does not make sense to enforce code coverage on some Maven modules such as those generated in GRPC.

In Jacoco, you can set different coverage limits for individual modules instead of a global threshold for all modules. In the following example, you can specify a coverage threshold for module A by modifying module A’s pom.xml file:

Module A's pom.xml
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
...

</plugins>
...
    <plugin>
        <groupId>com.atlassian.maven.plugins</groupId>
        <artifactId>maven-clover2-plugin</artifactId>
    </plugin>

    <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.7.9</version>

        <executions>
            <execution>
                <id>check</id>
                <goals>
                <goal>check</goal>
                </goals>
                <configuration>
                    <!-- NOTE: Set haltOnFailureto true when code coverage is enforced -->
                    <haltOnFailure>false</haltOnFailure>

                    <rules>
                        <rule >
                            <element>CLASS</element>
                            <limits>
                                <limit >
                                    <counter>LINE</counter>
                                    <value>COVEREDRATIO</value>
                                    <minimum>0.80</minimum>
                                </limit>
                                <limit >
                                    <counter>BRANCH</counter>
                                    <value>COVEREDRATIO</value>
                                    <minimum>0.80</minimum>
                                </limit>
                            </limits>
                            <excludes>
                                <!-- 
                                <exclude>com.test.ExampleExclusion</exclude>
                                -->
                            </excludes>
                        </rule>
                    </rules>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

As you can see, you can also specify files being excluded from coverage calculation.

References