Introduction
You've just shipped a feature. Tests pass. Deployment succeeds. Then two weeks later, a production bug surfaces in code that nobody thought to test. Code coverage with JaCoCo: practical tips and trade-offs isn't about chasing perfection—it's about being intentional. Coverage is a flashlight, not a target. It illuminates the dark corners of your codebase, but aiming for 100% often wastes team energy on trivial getters and generated boilerplate. After running hundreds of Java projects through CI/CD pipelines, we've learned what actually matters: knowing where coverage gaps exist, automating the measurement in your build, and using metrics to guard against regressions in code that truly matters.
In this article, we'll walk through generating JaCoCo reports in CI, finding untested critical paths, why chasing 100% is a trap, and how to fail builds strategically so coverage becomes a useful safety net rather than theater.
Generate reports in CI with the Maven plugin
Coverage tooling only works if it runs consistently. The easiest way to bake JaCoCo into your build is the Maven plugin, configured to run on every commit. When developers see coverage reports land in their pull request, they start thinking about it naturally—without a lecture.
Add this to your pom.xml:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>Run mvn clean test and JaCoCo generates an HTML report in target/site/jacoco/index.html. Open it in a browser and you see line-by-line coverage for every class. In CI, archive this artifact so reviewers can inspect it without running the build locally.
For GitHub Actions, that might look like:
- name: Run tests with coverage
run: mvn clean test
- name: Upload coverage reports
uses: actions/upload-artifact@v4
with:
name: jacoco-report
path: target/site/jacocoFor teams using SonarQube or other analysis platforms, JaCoCo can feed coverage data automatically. But even without those tools, the local HTML report is enough to spot patterns. The key habit: every merge should include a coverage run. No exceptions. Coverage that only runs on Fridays is coverage nobody uses.
Look for untested critical paths
Not all code is equal. A getter returning a constant field needs no test. A payment validation function needs multiple scenarios covered. The goal isn't 100% coverage; it's coverage where it matters.
When you open the JaCoCo report, start by filtering for red (uncovered) lines. Prioritize by asking: If this code breaks, do we lose revenue or customer trust? If yes, it needs testing. If it's a null check or a log statement, skip it.
Real example: an e-commerce checkout service.
@Service
public class CheckoutService {
public Order createOrder(CartRequest request) {
validatePaymentMethod(request.getPaymentId()); // CRITICAL
Order order = new Order(request);
order.calculateTax(request.getShippingAddress()); // CRITICAL
order.applyDiscount(request.getCouponCode()); // CRITICAL
paymentGateway.charge(order.getTotal()); // CRITICAL
return orderRepository.save(order);
}
private void validatePaymentMethod(String paymentId) {
if (paymentId == null) {
throw new InvalidPaymentException("Payment required");
}
}
}JaCoCo reports that validatePaymentMethod has 50% coverage—the null path is tested but the happy path isn't. That's a red flag. You can end up charging users with invalid payment methods. Test both branches. The same logic applies to discount calculations, tax rules, and shipping validations.
Contrast that with a utility function:
public class StringUtils {
public static String formatCurrency(BigDecimal amount) {
return "$" + amount.setScale(2, RoundingMode.HALF_UP).toString();
}
}If this has 80% coverage, is that a problem? Probably not, unless the missing 20% is a real edge case (like negative amounts). The pattern: coverage gaps matter most in business logic, validation, and state transitions. They matter least in utilities, view helpers, and trivial accessors.
Do not chase 100%
Teams that commit to 100% coverage often discover they're testing generated code, mocking away meaningful behavior, or writing tests that pass regardless of what the code does. It's performative testing, and it slows delivery.
Consider a Spring Data repository:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email);
List<User> findByCreatedDateAfter(LocalDateTime date);
}This is generated by Spring. Testing every derived query method is wasteful. If you do, you're testing Spring itself, not your code. Coverage tools will correctly mark these as uncovered—and that's fine. Exclude them.
To exclude classes or methods from JaCoCo, use the @Generated annotation or configure excludes in the plugin:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<configuration>
<excludes>
<exclude>**/config/**</exclude>
<exclude>**/entity/**</exclude>
</excludes>
</configuration>
<!-- ... -->
</plugin>A realistic coverage target for business-critical code is 70–85%. For infrastructure and utilities, 50–60% is acceptable. For generated or trivial code, 0% is fine. What matters is the discipline of knowing why coverage is where it is. If you can articulate "we're not testing this because it's a DTO" or "this is a logging edge case," you're in control. If you're hitting 100% by testing trivial paths, coverage is lying to you.
Fail builds only on drops in new code
The most powerful use of JaCoCo is regression detection. If you've been holding 72% coverage and a new PR drops it to 68%, that's a signal: investigate whether the new code lacks sufficient testing. But if you fail the build on absolute thresholds ("must be 80%"), you create an arms race where developers write dummy tests just to appease the tool.
JaCoCo has a rule feature that lets you enforce coverage rules per module or class:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>coverage-check</id>
<phase>test</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>PACKAGE</element>
<excludes>
<exclude>*Test</exclude>
</excludes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>This checks that every package maintains at least 60% line coverage. If a PR adds code and drops coverage below that threshold, the build fails. That's a reasonable bar. The developer then has two honest choices: write more tests, or explain why the code doesn't need them.
Better yet, integrate with a tool like Jacoco-Report or Codecov that tracks coverage trends over time and only alerts on regressions. That removes the noise and focuses on signal: did this change leave us worse off than we were?
Common mistakes
- Setting the threshold too high. Teams aiming for 90%+ coverage often resort to mocking behavior away or testing implementation details. You end up with brittle tests that break when you refactor. Aim for 65–75% on production code; it's more realistic and more valuable.
- Ignoring coverage in tests themselves. JaCoCo measures coverage of your main code, not your tests. But if your tests have no assertions, they don't validate anything. High coverage paired with weak assertions is a false positive. Code reviews should include assertion checks.
- Not excluding generated or configuration code. ORMs, Spring XML configs, build-generated beans, and Lombok-generated methods add noise to coverage reports. Exclude them early and keep reports focused on code you actually wrote.
- Checking coverage only at release time. Coverage should be part of the CI gate on every PR. If you wait until a release candidate, you're fixing coverage debt when you should be shipping. Build the habit early.
- Treating coverage as a guarantee of correctness. A well-tested function can still have logic bugs. Coverage tells you whether code has been exercised; it doesn't tell you whether it's correct. Pair it with code review, manual testing, and integration tests.
Key takeaways
- Configure JaCoCo in Maven and run it on every CI build. Archive reports so reviewers can inspect coverage without local setup.
- Identify critical business paths (validation, payment, state changes, calculations) and prioritize coverage there. Ignore trivial code like getters and logging.
- Target 60–75% coverage on production code. Higher targets incentivize fake tests. Coverage is a guide, not a scoreboard.
- Use coverage rules to catch regressions in new code, not to enforce absolute thresholds. "Did this PR drop coverage?" is a better question than "Is coverage above 80%?"
- Exclude generated, configuration, and trivial code from coverage analysis. Clean reports are more useful than comprehensive ones.
- Coverage without assertions is meaningless. Review tests for both coverage and assertion quality in code review.






Comments
Loading comments…