Quarkus - Measuring the coverage of your tests

Learn how to measure the test coverage of your application. This guide covers:

  • Measuring the coverage of your Unit Tests

  • Measuring the coverage of your Integration Tests

  • Separating the execution of your Unit Tests and Integration Tests

  • Consolidating the coverage for all your tests

Please note that code coverage is not supported in native mode.

1. Prerequisites

To complete this guide, you need:

  • less than 15 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.5.3+

  • Having completed the Testing your application guide

2. Architecture

The application built in this guide is just a JAX-RS endpoint (hello world) that relies on dependency injection to use a service.The service will be tested with JUnit 5 and the endpoint will be annotated via a @QuarkusTest annotation.

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git, or download an archive.

The solution is located in the tests-with-coverage-quickstart directory.

4. Starting from a simple project and two tests

Let’s start from an empty application created with the Quarkus Maven plugin:

  1. mvn io.quarkus:quarkus-maven-plugin:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=tests-with-coverage-quickstart
  4. cd tests-with-coverage-quickstart

Now we’ll be adding all the elements necessary to have an application that is properly covered with tests.

First, an application serving a hello endpoint:

  1. package org.acme.quickstart;
  2. import javax.inject.Inject;
  3. import javax.ws.rs.GET;
  4. import javax.ws.rs.Path;
  5. import javax.ws.rs.PathParam;
  6. import javax.ws.rs.Produces;
  7. import javax.ws.rs.core.MediaType;
  8. @Path("/hello")
  9. public class GreetingResource {
  10. private final GreetingService service;
  11. @Inject
  12. public GreetingResource(GreetingService service) {
  13. this.service = service;
  14. }
  15. @GET
  16. @Produces(MediaType.TEXT_PLAIN)
  17. @Path("/greeting/{name}")
  18. public String greeting(@PathParam("name") String name) {
  19. return service.greeting(name);
  20. }
  21. @GET
  22. @Produces(MediaType.TEXT_PLAIN)
  23. public String hello() {
  24. return "hello";
  25. }
  26. }

This endpoint uses a greeting service:

  1. package org.acme.quickstart;
  2. import javax.enterprise.context.ApplicationScoped;
  3. @ApplicationScoped
  4. public class GreetingService {
  5. public String greeting(String name) {
  6. return "hello " + name;
  7. }
  8. }

The project will also need some tests. First a simple JUnit:

  1. package org.acme.quickstart;
  2. import org.junit.jupiter.api.Assertions;
  3. import org.junit.jupiter.api.Test;
  4. public class GreetingServiceTest {
  5. @Test
  6. public void testGreetingService() {
  7. GreetingService service = new GreetingService();
  8. Assertions.assertEquals("hello Quarkus", service.greeting("Quarkus"));
  9. }
  10. }

But also a @QuarkusTest:

  1. package org.acme.quickstart;
  2. import io.quarkus.test.junit.QuarkusTest;
  3. import org.junit.jupiter.api.Test;
  4. import org.junit.jupiter.api.Tag;
  5. import java.util.UUID;
  6. import static io.restassured.RestAssured.given;
  7. import static org.hamcrest.CoreMatchers.is;
  8. @QuarkusTest
  9. @Tag("integration")
  10. public class GreetingResourceTest {
  11. @Test
  12. public void testHelloEndpoint() {
  13. given()
  14. .when().get("/hello")
  15. .then()
  16. .statusCode(200)
  17. .body(is("hello"));
  18. }
  19. @Test
  20. public void testGreetingEndpoint() {
  21. String uuid = UUID.randomUUID().toString();
  22. given()
  23. .pathParam("name", uuid)
  24. .when().get("/hello/greeting/{name}")
  25. .then()
  26. .statusCode(200)
  27. .body(is("hello " + uuid));
  28. }
  29. }

The first one will be our example of a Unit Test and the second one will be our example of Integration Test.

5. Separating executions of Unit Tests and Integration Tests

You may want to consider that JUnits and QuarkusTests are two different kind of tests and that they deserve to be separated. This way you could run them separately, in different cases or some more often than the others.In order to do so, we’ll use a feature of JUnit 5 that allows us to tag some tests. Let’s tag GreetingResourceTest.java and specify that it is an Integration Test:

  1. import org.junit.jupiter.api.Tag;
  2. ...
  3. @QuarkusTest
  4. @Tag("integration")
  5. public class GreetingResourceTest {
  6. ...
  7. }

We’re now able to distinguish unit tests and integration tests. Now, let’s bind them to different Maven lifecycle phases. Let’s use surefire to bind unit tests to the test phase and the integration tests to the integration-test phase.

  1. <project>
  2. ...
  3. <build>
  4. <plugins>
  5. <plugin>
  6. <artifactId>maven-surefire-plugin</artifactId>
  7. <version>${surefire-plugin.version}</version>
  8. <configuration>
  9. <excludedGroups>integration</excludedGroups>
  10. <systemProperties>
  11. <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
  12. </systemProperties>
  13. </configuration>
  14. <executions>
  15. <execution>
  16. <id>integration-tests</id>
  17. <phase>integration-test</phase>
  18. <goals>
  19. <goal>test</goal>
  20. </goals>
  21. <configuration>
  22. <excludedGroups>!integration</excludedGroups>
  23. <groups>integration</groups>
  24. </configuration>
  25. </execution>
  26. </executions>
  27. </plugin>
  28. </plugins>
  29. </build>
  30. ...
  31. </project>
This way, the QuarkusTest instances will be executed as part of the integration-test build phase while the other JUnit tests will still be ran during the test phase.You can run all the tests with the command ./mvnw clean verify (and you will notice that two tests are ran in different phases).

6. Measuring the coverage of JUnit tests using JaCoCo

It is now time to introduce JaCoCo to measure the coverage. The straightforward way to add JaCoCo to your build is to reference the plugin in your pom.xml.

  1. <properties>
  2. ...
  3. <jacoco.version>0.8.4</jacoco.version>
  4. </properties>
  5. <build>
  6. <plugins>
  7. ...
  8. <plugin>
  9. <groupId>org.jacoco</groupId>
  10. <artifactId>jacoco-maven-plugin</artifactId>
  11. <version>${jacoco.version}</version>
  12. <executions>
  13. <execution>
  14. <id>default-prepare-agent</id>
  15. <goals>
  16. <goal>prepare-agent</goal>
  17. </goals>
  18. </execution>
  19. <execution>
  20. <id>default-report</id>
  21. <goals>
  22. <goal>report</goal>
  23. </goals>
  24. <configuration>
  25. <dataFile>${project.build.directory}/jacoco.exec</dataFile>
  26. <outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory>
  27. </configuration>
  28. </execution>
  29. </executions>
  30. </plugin>
  31. </plugins>
  32. </build>
If you run ./mvnw clean test the coverage information will be collected during the execution of the unit tests in the file jacoco.exec.

7. Measuring separately the coverage of each test type

It is not strictly necessary, but let’s distinguish the coverage brought by each test type. To do so, we`ll just output the coverage info in two different files, one in jacoco-ut.exec and one in jacoco-it.exec.We also need to generate a separate report for each test execution. Let’s adjust the Jacoco configuration for that:

  1. <properties>
  2. ...
  3. <jacoco.version>0.8.4</jacoco.version>
  4. </properties>
  5. <build>
  6. <plugins>
  7. ...
  8. <plugin>
  9. <groupId>org.jacoco</groupId>
  10. <artifactId>jacoco-maven-plugin</artifactId>
  11. <version>${jacoco.version}</version>
  12. <executions>
  13. <execution>
  14. <id>prepare-agent-ut</id>
  15. <goals>
  16. <goal>prepare-agent</goal>
  17. </goals>
  18. <configuration>
  19. <destFile>${project.build.directory}/jacoco-ut.exec</destFile>
  20. </configuration>
  21. </execution>
  22. <execution>
  23. <id>prepare-agent-it</id>
  24. <phase>pre-integration-test</phase>
  25. <goals>
  26. <goal>prepare-agent</goal>
  27. </goals>
  28. <configuration>
  29. <destFile>${project.build.directory}/jacoco-it.exec</destFile>
  30. </configuration>
  31. </execution>
  32. <execution>
  33. <id>report-ut</id>
  34. <goals>
  35. <goal>report</goal>
  36. </goals>
  37. <configuration>
  38. <dataFile>${project.build.directory}/jacoco-ut.exec</dataFile>
  39. <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
  40. </configuration>
  41. </execution>
  42. <execution>
  43. <id>report-it</id>
  44. <phase>post-integration-test</phase>
  45. <goals>
  46. <goal>report</goal>
  47. </goals>
  48. <configuration>
  49. <dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
  50. <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
  51. </configuration>
  52. </execution>
  53. </executions>
  54. </plugin>
  55. </plugins>
  56. </build>

8. The coverage does not seem to correspond to the reality

You can now run the tests: ./mvnw clean verify As explained earlier, it will run the unit tests first, then the integration tests. And finally, it will generate two separate reports. First a report of the coverage of the unit tests in target/site/jacoco-ut then a report of the coverage of the integration tests in target/site/jacoco-it.

Given the content of GreetingResourceTest, GreetingResource should have been covered. But when we open the report target/site/jacoco-it/index.html, the class GreetingResource is reported with 0% of coverage. But the fact that GreetingService is reported as covered shows that the test execution was actually recorded. How come?

During the report generation, you may have noticed a warning:

  1. [WARNING] Classes in bundle '***' do no match with execution data. For report generation the same class files must be used as at runtime.
  2. [WARNING] Execution data for class org/acme/quickstart/GreetingResource does not match.

It seems that Quarkus and JaCoCo step on each other’s toes. What happens is that Quarkus transforms the JAX-RS resources (and also the Panache files).You may have noticed that GreetingResource was not written in the simplest way like:

  1. ...
  2. @Path("/hello")
  3. public class GreetingResource {
  4. @Inject
  5. GreetingService service;
  6. @GET
  7. @Produces(MediaType.TEXT_PLAIN)
  8. @Path("/greeting/{name}")
  9. public String greeting(@PathParam("name") String name) {
  10. return service.greeting(name);
  11. }
  12. @GET
  13. @Produces(MediaType.TEXT_PLAIN)
  14. public String hello() {
  15. return "hello";
  16. }
  17. }

Above, the constructor is implicit and we use injection to have an instance of GreetingService. Note that, with this code relying on an implicit constructor, the coverage would have been reported properly by JaCoCo.Instead, we introduced a constructor based injection:

  1. ...
  2. @Path("/hello")
  3. public class GreetingResource {
  4. private final GreetingService service;
  5. @Inject
  6. public GreetingResource(GreetingService service) {
  7. this.service = service;
  8. }
  9. ...
  10. }

Some might say that this approach is preferable since the field can be final like this. Anyway, in some cases you might need an explicit constructor. And, in that case, the coverage is not reported properly by JaCoCo.This is because Quarkus generates a constructor without any parameter and does some bycode manipulations in order to add it to the class. That is what happened here, just before the execution of the integration tests:

  1. [INFO] --- quarkus-maven-plugin:0.16.0:build (default) @ getting-started-testing ---
  2. [INFO] [io.quarkus.deployment.QuarkusAugmentor] Beginning quarkus augmentation
  3. ...

As a consequence, JaCoCo does not recognize the classes when it wants to create its report. But wait…​ there is a solution.

9. Instrumenting the classes instead

JaCoCo has two modes. The first one is based on an agent and instruments classes on-the-fly. Unfortunately, this is incompatible with the dynamic classfile transformations that Quarkus does. The second mode is called offline instrumentation. Classes are pre-instrumented in advance via the jacoco:instrument Maven goal and during their usage (when the tests are ran), jacocoagent.jar must be added to the classpath.Once the tests have been executed, it is recommended to restore the original classes using the jacoco:restore-instrumented-classes Maven goal.

Let’s first add the dependency on jacocoagent.jar:

  1. <project>
  2. ...
  3. <dependencies>
  4. ...
  5. <dependency>
  6. <groupId>org.jacoco</groupId>
  7. <artifactId>org.jacoco.agent</artifactId>
  8. <classifier>runtime</classifier>
  9. <scope>test</scope>
  10. <version>${jacoco.version}</version>
  11. </dependency>
  12. </dependencies>
  13. </project>

Then let’s configure three jacoco plugin goals for unit tests:

  • One to instrument the classes during the process-classes phase

  • One to restore the original classes during the prepare-package phase (after the tests are ran)

  • One to generate the report during the verify phase (the report generation requires the original classes to have been restored)

and a similar setup for the integration tests too:

  1. <project>
  2. ...
  3. <build>
  4. <plugins>
  5. ...
  6. <plugin>
  7. <groupId>org.jacoco</groupId>
  8. <artifactId>jacoco-maven-plugin</artifactId>
  9. <version>${jacoco.version}</version>
  10. <executions>
  11. <execution>
  12. <id>instrument-ut</id>
  13. <goals>
  14. <goal>instrument</goal>
  15. </goals>
  16. </execution>
  17. <execution>
  18. <id>restore-ut</id>
  19. <goals>
  20. <goal>restore-instrumented-classes</goal>
  21. </goals>
  22. </execution>
  23. <execution>
  24. <id>report-ut</id>
  25. <goals>
  26. <goal>report</goal>
  27. </goals>
  28. <configuration>
  29. <dataFile>${project.build.directory}/jacoco-ut.exec</dataFile>
  30. <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
  31. </configuration>
  32. </execution>
  33. <execution>
  34. <id>instrument-it</id>
  35. <phase>pre-integration-test</phase>
  36. <goals>
  37. <goal>instrument</goal>
  38. </goals>
  39. </execution>
  40. <execution>
  41. <id>restore-it</id>
  42. <phase>post-integration-test</phase>
  43. <goals>
  44. <goal>restore-instrumented-classes</goal>
  45. </goals>
  46. </execution>
  47. <execution>
  48. <id>report-it</id>
  49. <phase>post-integration-test</phase>
  50. <goals>
  51. <goal>report</goal>
  52. </goals>
  53. <configuration>
  54. <dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
  55. <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
  56. </configuration>
  57. </execution>
  58. </executions>
  59. </plugin>
  60. </plugins>
  61. </build>
  62. </project>

It also requires a small change in the Surefire configuration. Note below that we specified jacoco-agent.destfile as a system property in the default case (unit tests) and for the integration tests.

  1. <project>
  2. ...
  3. <build>
  4. <plugins>
  5. ...
  6. <plugin>
  7. <artifactId>maven-surefire-plugin</artifactId>
  8. <version>${surefire-plugin.version}</version>
  9. <configuration>
  10. <excludedGroups>integration</excludedGroups>
  11. <systemProperties>
  12. <jacoco-agent.destfile>${project.build.directory}/jacoco-ut.exec</jacoco-agent.destfile>
  13. <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
  14. </systemProperties>
  15. </configuration>
  16. <executions>
  17. <execution>
  18. <id>integration-tests</id>
  19. <phase>integration-test</phase>
  20. <goals>
  21. <goal>test</goal>
  22. </goals>
  23. <configuration>
  24. <excludedGroups>!integration</excludedGroups>
  25. <groups>integration</groups>
  26. <systemProperties>
  27. <jacoco-agent.destfile>${project.build.directory}/jacoco-it.exec</jacoco-agent.destfile>
  28. </systemProperties>
  29. </configuration>
  30. </execution>
  31. </executions>
  32. </plugin>
  33. </plugins>
  34. </build>
  35. </project>

Let’s now check the generated report that can be found in target/site/jacoco-it/index.html. The report now shows that GreetingResource is actually properly covered! Yay!

10. Bonus: Building a consolidated report for Unit Tests and Integration Tests

So, finally, let’s improve the setup even further and let’s merge the two execution files (jacoco-ut.exec and jacoco-it.exec) into one consolidated report and generate a consolidated report that will show the coverage of all your tests combined.

You should end up with something like this (note the addition of the merge-results and post-merge-report executions):

  1. <project>
  2. ...
  3. <build>
  4. <plugins>
  5. <plugin>
  6. <artifactId>maven-surefire-plugin</artifactId>
  7. <version>${surefire-plugin.version}</version>
  8. <configuration>
  9. <excludedGroups>integration</excludedGroups>
  10. <systemProperties>
  11. <jacoco-agent.destfile>${project.build.directory}/jacoco-ut.exec</jacoco-agent.destfile>
  12. <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
  13. </systemProperties>
  14. </configuration>
  15. <executions>
  16. <execution>
  17. <id>integration-tests</id>
  18. <phase>integration-test</phase>
  19. <goals>
  20. <goal>test</goal>
  21. </goals>
  22. <configuration>
  23. <excludedGroups>!integration</excludedGroups>
  24. <groups>integration</groups>
  25. <systemProperties>
  26. <jacoco-agent.destfile>${project.build.directory}/jacoco-it.exec</jacoco-agent.destfile>
  27. </systemProperties>
  28. </configuration>
  29. </execution>
  30. </executions>
  31. </plugin>
  32. ...
  33. <plugin>
  34. <groupId>org.jacoco</groupId>
  35. <artifactId>jacoco-maven-plugin</artifactId>
  36. <version>${jacoco.version}</version>
  37. <executions>
  38. <execution>
  39. <id>instrument-ut</id>
  40. <goals>
  41. <goal>instrument</goal>
  42. </goals>
  43. </execution>
  44. <execution>
  45. <id>restore-ut</id>
  46. <goals>
  47. <goal>restore-instrumented-classes</goal>
  48. </goals>
  49. </execution>
  50. <execution>
  51. <id>report-ut</id>
  52. <goals>
  53. <goal>report</goal>
  54. </goals>
  55. <configuration>
  56. <dataFile>${project.build.directory}/jacoco-ut.exec</dataFile>
  57. <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
  58. </configuration>
  59. </execution>
  60. <execution>
  61. <id>instrument-it</id>
  62. <phase>pre-integration-test</phase>
  63. <goals>
  64. <goal>instrument</goal>
  65. </goals>
  66. </execution>
  67. <execution>
  68. <id>restore-it</id>
  69. <phase>post-integration-test</phase>
  70. <goals>
  71. <goal>restore-instrumented-classes</goal>
  72. </goals>
  73. </execution>
  74. <execution>
  75. <id>report-it</id>
  76. <phase>post-integration-test</phase>
  77. <goals>
  78. <goal>report</goal>
  79. </goals>
  80. <configuration>
  81. <dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
  82. <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
  83. </configuration>
  84. </execution>
  85. <execution>
  86. <id>merge-results</id>
  87. <phase>verify</phase>
  88. <goals>
  89. <goal>merge</goal>
  90. </goals>
  91. <configuration>
  92. <fileSets>
  93. <fileSet>
  94. <directory>${project.build.directory}</directory>
  95. <includes>
  96. <include>*.exec</include>
  97. </includes>
  98. </fileSet>
  99. </fileSets>
  100. <destFile>${project.build.directory}/jacoco.exec</destFile>
  101. </configuration>
  102. </execution>
  103. <execution>
  104. <id>post-merge-report</id>
  105. <phase>verify</phase>
  106. <goals>
  107. <goal>report</goal>
  108. </goals>
  109. <configuration>
  110. <dataFile>${project.build.directory}/jacoco.exec</dataFile>
  111. <outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory>
  112. </configuration>
  113. </execution>
  114. </executions>
  115. </plugin>
  116. </plugins>
  117. </build>
  118. ...
  119. <dependencies>
  120. ...
  121. <dependency>
  122. <groupId>org.jacoco</groupId>
  123. <artifactId>org.jacoco.agent</artifactId>
  124. <classifier>runtime</classifier>
  125. <scope>test</scope>
  126. <version>${jacoco.version}</version>
  127. </dependency>
  128. </dependencies>
  129. </project>

11. Conclusion

You now have all the information you need to study the coverage of your tests!But remember, some code that is not covered is certinaly not well tested. But some code that is covered is not necessarily well tested. Make sure to write good tests!