Skip to content
Source

About Test Coverage

There's surprisingly little information on the internet about how to technically set up increasing test coverage levels on projects where, for historical reasons, it wasn't customary to write them. Usually tests are written by big and cool folks who once set this up and don't see the point in talking about it. And if they do talk about it, it's not easy to apply to yourself.

So you decided to write tests. Let's assume you've figured out the whole stack you'll do this with. Although that's also quite a task. There are many frameworks, approaches within them, and even assertion libraries are not just one or two. The main difficulty isn't that you can't write tests, but that people actually write them.

The only metric that makes sense to me is the percentage of code lines covered by tests. For collecting this there's JaCoCo, and there's Kover. The first is an industry standard, but doesn't work well with Kotlin. The second - although it hasn't yet released a stable version, it's from JetBrains, with Kotlin in mind, and seems much simpler. So we're betting on it. Copying from documentation just doesn't work. Adding coverage collection to each of ~200 project modules is simple, but gathering this into one final report takes some work. Samples are somewhere at the bottom of GitHub and you search among acquaintances.

Kover makes reports in JaCoCo format, which is quite popular, so any further integrations should be equally simple. And indeed, adding parsing of this report to GitLab CI is fairly easy, for example. And now on code review in GitLab, some uncovered lines start to be highlighted in red and the merge request proudly displays a number - 2% project coverage.

And here lies the next problem. What to do with this? You can't just say that starting tomorrow we won't merge your requests without 80% coverage. The developer in their merge request has nothing to do with the project not being covered. They should only be responsible for their specific request. Project coverage, while interesting, is a useless thing, because you can't do anything with this number. It would be fine if this was a new project where you could immediately fix the required percentage. But we have an old one.

So I came to the idea that the key metric is the percentage of covered lines specifically at the level of a specific merge request. We should require from developers that they cover their new/changed lines to the target 80%. But how do you do this if we have a report for the entire project and GitLab can't calculate this itself?

The task, it would seem, is algorithmically simple - take the diff, take the JaCoCo report, calculate the number of covered lines from the diff relative to all lines from the diff - return the coverage percentage. Surely someone has done this, I'd even venture to assume there are ready-made GitHub Actions or some SonarQube can do this out of the box. But what if you don't have them?

A surface search shows that they did. But there are fewer and fewer solutions, for you a hundred stars on GitHub is already a joy. There's something like this or this. I don't even remember what was wrong with them, but none of them worked for me in several hours the way I wanted. Maybe I didn't try hard enough, but it seems like the hassle is disproportionate to the task.

If you thought this post would get by without AI agents, you were wrong. As a result, I went and in a few hours generated a Gradle task that takes a coverage report, a commit hash for which it runs git diff, and matches lines with the report. The output is one number - the coverage percentage at the diff level. And I generated tests. And documentation. And fixed CI configs so it runs. And it all works.

Well, and then it's just a matter of technique. We can, for example, look at the result of this task in danger, praise the developer if everything is good / say that it could be better / say they should go write more tests, otherwise it won't merge.

A bicycle - yes. Solves the task - yes. Did I have fun writing this - yes.

And then there's a fork. Either you tell me I'm wrong and should have done it this way or that, throw some materials. Or I put this simplest solution in a GitHub repo so it can be connected with one line.