I’m a nobody, nobody is perfect, therefore I’m perfect.

JaCoCo / Gradle — Excluding Source Files and Classes

I have switched recently from Cobertura to JaCoCo so I started becoming more familiar with this tool. (And since you asked, one of the main reasons for the switch is that Cobertura is absolutely terrible when it comes to Java lambdas and pollutes the output with a lot of warning and messages about malformed Java code ?!?! Also it has some oddities when dealing with Groovy code and last but not least it is rather heavy on the system.)

I didn’t expect this migration to be than painful and truth being told it wasn’t. I use Gradle for my builds nowadays so my first go-to was the JaCoCo Gradle plugin docco. That page is pretty straight forward and walked me through most of the stuff I needed. There was one feature though that took a bit of time for me to implement same as I had in Cobertura — and this is what triggered my publishing this blog post.

My projects in Netflix nowadays use Guice a lot — and as such I find myself implementing often Modules, Providers and all sorts of dependency injection Guice specifics. In some cases I create a Java package to isolate all the Guice stuff in (typically named something like 'x.y.z.guice'), in some cases (especially with the legacy projects) they end up everywhere throughout the project. But in both of these scenarios I ended up excluding these classes and packages from the Cobertura analysis. With Cobertura this is easy you just specify a regex which excludes either classes called '*Module' or '*Provider' etc. or in a packaged called 'x.y.z.guice.*'.

Now the Gradle JaCoCo plugin has a property excludes and the task docco reads :

List of class names that should be excluded from analysis. Names can use wildcard (* and ?). Defaults to an empty list.

Great! So, assuming I want to exclude a class called “GuiceModule” all I have to do is set it like this:

jacocoTestReport {
excludes =['*GuiceModule']}

jacocoTestReport {
excludes = ['*GuiceModule']
}

Right? Well… sort of, as it turns out!

The above setting excludes the class from the generated report — but (and this is a subtle difference) not from the actual analysis! Let’s look at this example I put on GitHub to see the difference.

It’s a very simple Java project with 2 classes: Library and OtherLibrary — with only Library being unit-tested (if you can call that unit testing :D). Now let’s say that the OtherLibrary — if we were to take the real-life case I mentioned before regarding Guice — is a Guice module which I don’t want to test. (Seriously, who unit-tests those???) As such I want this excluded from my JaCoCo — don’t want my reports polluted by it. So I set the build file as follows:

As you can see it’s pretty straight forward: I just want to exclude anything called '*OtherLibrary*' — which would of course include my OtherLibrary class! If I run the above build and look at the JaCoCo reports this is what I see:

Wow so hang on, my class still appears in the report! And in fact the data is still being aggregated in the report: you can notice that the number of lines and missed and so on. And in fact if you look at the aggregate report for the “default” package it reports … 42% coverage! Even though as far as I’m concerned all the code that is important is unit tested — and as such that figure should be (at least close to) 100%!

So it seems that taking this route doesn’t really achieve what I want!

(As a side note, I wish JaCoCo implemented the feature that Cobertura has about failing a build if a minimum coverage level is not met, as that keeps your developers honest. However, just as well it doesn’t, because every Guice module would bring the coverage — wrongly! — down!)

Digging around it seems the solution lies in eliminating the classes from the classpath of the plugin — so JaCoCo never actually sees the class, and as such doesn’t aggregate the numbers. This is a more convoluted solution but does just what I want. Here’s how to eliminate them from the classpath — still using some regex:

As you can see we use a closure to traverse the “class” directory and eliminate files which match the pattern. Now when I run the build again, I get:

Yessss! Result! My OtherLibrary is gone and the numbers from it are not aggregated — and if you look at the aggregated report at “default” package level you will see that the coverage level is now (rightfully!) to 100%.