Know What You’re Testing: Mastering Code Coverage in Quarkus with JaCoCo
A hands-on guide for Java developers to generate, visualize, and improve test coverage in Quarkus applications using JaCoCo.
This tutorial will walk you through the essentials of test coverage in Quarkus. You'll learn how to generate coverage reports, understand what they mean, and use them to improve your tests. We'll be using JaCoCo, the de facto standard for code coverage in Java, which integrates smoothly with Quarkus.
Why Bother with Test Coverage?
You’ve written tests. They pass. CI is green. But… are they actually doing anything useful? Test coverage helps answer that. Test coverage is a metric, usually expressed as a percentage, that quantifies how much of your application's source code is executed when your test suite runs. Common coverage criteria include:
Line coverage: Percentage of executable lines of code run.
Branch coverage: Percentage of conditional branches (e.g.,
if
/else
paths) taken.
Why is it Important?
Identifies Untested Code: It clearly highlights parts of your application that your tests don't touch.
Boosts Confidence: Higher coverage can increase confidence in your test suite's ability to catch regressions.
Guides Test Writing: It helps you focus your efforts on writing tests for critical, uncovered areas.
Caveat: High coverage doesn't guarantee bug-free code or perfectly designed tests. It simply means the code was executed. The quality and assertions within your tests are paramount. However, low coverage is a definite red flag!
Quarkus provides excellent support for JaCoCo. The quarkus-jacoco
extension simplifies the setup, allowing you to focus on writing code and tests.
Let’s get started.
Prerequisites
JDK 17+
Apache Maven 3.8.1+
Quarkus CLI (optional)
Podman (optional)
A terminal and browser — that’s all you need to explore the reports
Step 1: Scaffold Your Project
Generate a minimal Quarkus REST app:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=quarkus-coverage-tutorial \
-DclassName="org.acme.GreetingResource" \
-Dpath="/hello" \
-Dextensions="rest, quarkus-jacoco"
cd quarkus-coverage-tutorial
Start it up:
./mvnw quarkus:dev
Check: Visit http://localhost:8080/hello to see the default greeting.
Step 2: Add Some Business Logic
Let’s build a simple GreetingService
that returns greetings and farewells.
src/main/java/org/acme/GreetingService.java
package org.acme;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class GreetingService {
public String greet(String name) {
if (name == null || name.trim().isEmpty()) {
return "Hello, stranger!";
}
return "Hello, " + name + "!";
}
public String farewell(String name) {
return "Goodbye, " + name + "!";
}
}
We've added a couple of conditions in greet
and a new farewell
method.
Modify GreetingResource
to use GreetingService
: Update the existing src/main/java/org/acme/GreetingResource.java
:
package org.acme;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
GreetingService service;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello(@QueryParam("name") String name) {
return service.greet(name);
}
// Let's add another endpoint for farewell
@GET
@Path("/goodbye")
@Produces(MediaType.TEXT_PLAIN)
public String goodbye(@QueryParam("name") String name) {
return service.farewell(name);
}
}
Step 3: Write Basic Tests
Quarkus generated a GreetingResourceTest.java
. Let's adapt it and also create a dedicated test for our GreetingService
.
Modify the the REST resource test:
src/test/java/org/acme/GreetingResourceTest.java
package org.acme;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class GreetingResourceTest {
@Test
public void testHelloEndpointWithName() {
given()
.queryParam("name", "Dev")
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Dev!"));
}
@Test
public void testHelloEndpointNoName() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, stranger!"));
}
// No test for /hello?name=admin
// No test for /hello/goodbye endpoint yet
}
Test the service directly:
Create new test: src/test/java/org/acme/GreetingServiceTest.java
package org.acme;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
@QuarkusTest // This ensures the service can be @Inject'ed
public class GreetingServiceTest {
@Inject
GreetingService greetingService;
@Test
public void testGreetWithName() {
assertEquals("Hello, Quarkus!", greetingService.greet("Quarkus"));
}
@Test
public void testGreetNullName() {
assertEquals("Hello, stranger!", greetingService.greet(null));
}
@Test
public void testGreetEmptyName() {
assertEquals("Hello, stranger!", greetingService.greet(""));
}
// We are intentionally NOT testing the "admin" branch in greet()
// and NOT testing the farewell() method yet to see them uncovered.
}
Step 4: Enable JaCoCo in Quarkus
Quarkus adds JaCoCo as dependency. You should make sure to adjust the scope to test in the pom.xml:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jacoco</artifactId>
<scope>test</scope>
</dependency>
Run the tests:
./mvnw clean test
This generates a .exec
file and an HTML report in the target/
folder.
Enable Automatic Report Generation:
For simple report generation without needing to configure the jacoco-maven-plugin
explicitly for the report goal, Quarkus provides a convenient property. Add the following line to your src/main/resources/application.properties
file:
quarkus.jacoco.report=true
You can find the full list of properties that influence report generations on the Quarkus Guide about Test-Coverage.
Step 5: Analyze the Coverage Report
Open the HTML file:
open target/site/jacoco/index.html # macOS
# OR
xdg-open target/site/jacoco/index.html # Linux
When you open the JaCoCo coverage report, the Overview page presents a summary table listing all packages along with their overall coverage metrics, such as instructions, branches, cyclomatic complexity, lines, methods, and classes. From there, you can drill down into the Package view by clicking on your package name (e.g., org.acme
), which reveals coverage details for each class within that package. Selecting a specific class, such as GreetingService.java
or GreetingResource.java
, brings you to the Class view, where the real insights appear. Here, the source code is highlighted based on coverage: lines with a green background were fully executed during tests; yellow backgrounds or diamonds indicate partial coverage, typically where not all branches in a conditional were hit; and red highlights or diamonds mark lines or branches not executed at all: These are your best targets for improving test coverage.
GreetingService
: Thegreet()
method is green ✅.farewell()
is red ❌ — never tested.GreetingResource
: Both paths covered by endpoint tests.
Step 6: Increase Coverage
Let’s fix that missing test.
Open src/test/java/org/acme/GreetingServiceTest.java
and add the missing tests:
@Test
public void testGreetAdmin() {
assertEquals("Welcome, admin! Privileged access granted.", greetingService.greet("admin"));
}
@Test
public void testFarewellWithName() {
assertEquals("Goodbye, TestUser!", greetingService.farewell("TestUser"));
}
@Test
public void testFarewellNullName() {
assertEquals("Goodbye, mysterious one!", greetingService.farewell(null));
}
Re-run tests and refresh the report. Everything should now be green!
Step 7: Enforce Coverage in CI (Optional)
While quarkus.jacoco.report=true
is convenient, for CI pipelines or more customization, you'll typically use the jacoco-maven-plugin
directly.
Automate in CI: Integrate the test and coverage generation steps into your CI pipeline (e.g., Jenkins, GitLab CI, GitHub Actions).
Fail Build on Low Coverage: You can configure the
jacoco-maven-plugin
to enforce coverage thresholds and fail the build if they aren't met. This helps maintain a minimum level of test quality.To do this, you would first remove or set
quarkus.jacoco.report=false
inapplication.properties
if you want thejacoco-maven-plugin
to handle all reporting. Then, configure the plugin in yourpom.xml
within the<build><plugins>
section:
Add JaCoCo enforcement rules:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<goals><goal>report</goal></goals>
</execution>
<execution>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>CLASS</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
Now a test run will fail if coverage drops below 80%.
Wrap-Up
In less than 30 minutes, you’ve:
Created a Quarkus app with a simple service
Wrote both unit and endpoint tests
Generated and viewed JaCoCo test coverage reports
Improved your test suite by catching untested logic
Learned how to enforce coverage thresholds in CI
Remember: Coverage is just a signal. It tells you where your tests aren’t reaching. Combine it with thoughtful test design, and you’ll ship code with confidence.
Now go hunt down that red in your own codebase.