Measuring Grails test coverage with JaCoCo reports
For teams shipping Grails web applications from Brisbane, Melbourne, or anywhere else in Australia, knowing which parts of a Groovy codebase actually got exercised by the test suite is more than a vanity metric. The JaCoCo library plugs straight into the JVM toolchain that Grails already relies on, so collecting coverage data does not require a separate agent process or a heavyweight commercial licence. Once the report is wired into a Gradle build, every grails test-app invocation can leave behind a folder full of HTML, XML, and CSV files that a developer can open locally, archive as a build artefact, or feed to a dashboard.
What makes JaCoCo particularly friendly inside a Grails project is that it instruments compiled class files at runtime through a Java agent, meaning the source code itself never needs to be touched. The build tool handles the agent wiring transparently, and the resulting report highlights covered branches, missed lines, and cyclomatic complexity on a per-package basis. For Australian teams juggling time zones between Perth and Sydney while waiting on a shared CI farm, that low-friction setup tends to pay off the first time a regression slips past a developer and lands in a pull request review.
Beyond the engineering argument, coverage reporting ties neatly into local expectations around software assurance. Procurement teams in the financial sector around Martin Place and government departments bound by the Australian Signals Directorate's Essential Eight mitigation strategies all want evidence that quality gates were applied. A reproducible JaCoCo report is exactly that: a timestamped, machine-readable artefact that proves which release candidate was measured, and what the numbers looked like.
The role of code coverage in Grails projects
Coverage data is a guide, not a verdict. A line counter that says eighty-five percent of statements were executed tells you very little about whether the assertions inside those statements actually verified business behaviour. For a Grails controller, a hundred percent line coverage can still hide a domain class invariant that gets violated the moment a production database hands back a null. The value of JaCoCo is that it surfaces those gaps visually, package by package, so a reviewer can scan the report and ask sharper questions about what was missing.
For a typical Grails application split across controllers, services, domain, and conf packages, JaCoCo will produce a per-class breakdown showing which GSP render paths, which service methods, and which constraints were reached. This granularity is what makes the report useful in code review. A quick eyeball of the index page often shows up a controller that was tested only via the integration layer but never through the actual web request lifecycle, which is a common blind spot when a project leans heavily on Spock specifications.
The pragmatic question for any team weighing JaCoCo is whether the time spent maintaining the configuration pays back in fewer escaped defects. In shops where releases are batched around business days and the on-call rotation crosses the country, fewer late-night pages usually justify a few minutes of build script editing. The rest of this article walks through the practical steps of making that investment.
Adding JaCoCo to a Gradle build
Most contemporary Grails projects use Gradle rather than the older Gant-based build, and the JaCoCo plugin drops in with a single line. Inside build.gradle, adding id 'jacoco' to the plugins block is enough to expose the jacocoTestReport task, which runs after the test task and writes a default HTML report to build/reports/jacoco/test/html. A few properties on the jacoco extension control the tool version and the rule set applied during enforcement.
A sensible baseline for a Grails 5 or 6 project looks like this:
jacoco {
toolVersion = '0.8.11'
}
jacocoTestReport {
dependsOn test
reports {
html.required = true
xml.required = true
csv.required = false
}
}
The XML output is the version that gets consumed by most CI dashboards and by tools like SonarQube, so leaving it enabled is usually the right call. For teams that archive build artefacts to an S3 bucket in the ap-southeast-2 Sydney region, the XML file is small enough to ship alongside the WAR without bloating the artefact.
One detail that catches first-time users is that Grails generates two distinct test source sets, unit and integration, and the JaCoCo agent only instruments the classes that are actually loaded during the test run. If a controller is never referenced by an integration spec, the report will show zero coverage for that class even if unit tests cover the underlying service. Splitting the report task to run after both source sets is the usual fix, and the lifecycle script guide walks through how to chain those tasks without re-running the suite twice.
Reading the HTML and XML reports
The HTML report groups results by package, then by class, then by method, with a colour gradient that runs from green for fully covered code to red for lines never executed. Branch coverage, which JaCoCo tracks separately from line coverage, is shown with a small yellow diamond on the relevant control flow node. For a Groovy service that returns one of three possible statuses from a switch statement, the diamond tells you whether each case was actually visited or whether the test suite only ever exercised the first branch.
In practice, the HTML report is most useful during local development. A developer running ./gradlew test jacocoTestReport can open the index page, click into the package of interest, and immediately see which class is dragging the overall figure down. The numbers are also sorted, so the worst-performing classes sit at the top of the list, which makes triage a quick scroll rather than a hunt.
The XML report, by contrast, is the file that downstream systems read. SonarQube, CodeClimate, Codecov, and a long list of in-house dashboards all expect the JaCoCo XML schema, and a single file can be uploaded once per build. A team that publishes artefacts to an internal Nexus repository can configure the CI job to attach the XML as a secondary artefact, alongside the WAR, without any extra plugin gymnastics. The schema is stable enough that upgrades to the JaCoCo tool version rarely break a downstream parser, which is one of the reasons the library has stayed popular for the better part of a decade.
Wiring coverage into CI pipelines
Running coverage locally is fine for the developer who already cares about quality, but the real leverage comes from automating the report inside the CI pipeline. For teams using GitHub Actions runners hosted in ap-southeast-2, the workflow needs to invoke the same Gradle tasks a developer would run locally, then archive the resulting build/reports/jacoco folder as a downloadable artefact. The whole exercise adds only a handful of seconds to a typical build.
A practical pipeline step looks like this:
- name: Run tests with coverage
run: ./gradlew test jacocoTestReport
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: jacoco-report
path: build/reports/jacoco/test/html
Some teams prefer to fail the build outright when coverage drops below a threshold, and the JaCoCo Gradle integration supports that pattern through jacocoTestReport configuration blocks. A common rule is to require seventy percent line coverage for services and domain classes, with a lower bar for generated code such as GSP views. Failing fast in CI means a pull request that drags the average down gets caught at review time rather than surfacing weeks later in a quarterly quality review.
For projects with stricter obligations, such as software shipped to a Commonwealth entity bound by IRAP controls, the XML report also serves as auditable evidence that the released artefact was measured. The Grails tutorials on the site cover several variations of this pattern, from a single-developer hobby app up through multi-module enterprise builds.
Coverage thresholds and quality gates
Enforcement is where most teams find their footing, and the simplest expression of a quality gate is a numeric floor. JaCoCo's violationRules block accepts a limit for both line and branch coverage, and the build will fail if any package falls below that limit. A reasonable starting point for a Grails application is seventy percent line coverage with sixty percent branch coverage, then ratcheting those figures upward over successive sprints as the test suite matures.
The trick is to apply the rule at the right scope. Setting a global minimum across the whole codebase is a blunt instrument that punishes packages that have nothing to do with the change being shipped. A more useful pattern is to enforce the rule on the diff itself, comparing the coverage of the changed classes against the configured threshold, which is how most modern review tools work. A developer submitting a pull request can see, before the CI job even finishes, whether their change leaves a new class below the agreed bar.
There is also a cultural dimension worth naming. Coverage thresholds work best when they are visible and agreed upon, not when they are quietly tuned in a shared Gradle file that nobody reads. Posting the report link in the team channel after each release builds the habit of looking at the numbers. Over time, the report stops feeling like a chore and starts feeling like the cheapest, fastest way to spot a half-tested service before it reaches production.
For developers getting started with the toolchain, a solid grounding in machine learning workflows with Java libraries is a useful complement to coverage reporting, since both rely on the same kind of structured feedback loop. The two practices reinforce each other: a model that has not been evaluated is just a guess, and a service that has not been tested is just code. Treat the JaCoCo report as the evaluation step for the test suite, and the rest of the engineering discipline tends to fall into place without too much fuss.