Skip to content

Adopt Java Method Filtration JaCoCo Extension #161

@tmikula-dev

Description

@tmikula-dev

Background

We use JaCoCo for instruction-based code coverage measurement with Scala code.
Scala introduces a significant amount of boilerplate which makes final reports inaccurate and inflates method counts with code that has no meaningful value to test.

Feature

  1. Main: Adopt the JaCoCo Method Filter (JMF) plugin to enable filtering on the method level, cleanly excluding Scala-generated boilerplate from coverage reports.
  2. Optional: Set Up the JaCoCo GitHub Actions Workflow

Business Value

Code coverage reports will reflect accurate, meaningful numbers that the team can trust and act on. Team can better focus on biggest coverage gaps - per module or inside each module.

Reference Example of JMF

A pilot implementation has been completed in the balta and Unify projects. The following files from that project can be used as a reference:

Proposed Solution of JMF

Follow the steps below, skipping any that do not apply to your project.

The full code coverage solution consists of two parts:

  1. Project-level JaCoCo setup — enables local test runs and report generation
  2. GitHub Actions workflow — runs tests with JaCoCo and posts a PR comment with coverage stats

1. Remove Existing JaCoCo Setup (if applicable)

Skip this section if JaCoCo is not already configured in your project.

  1. Back up any JaCoCo-related settings for later reference (report titles, formats, file exclusions, thresholds, etc.).
  2. Remove the existing JaCoCo configuration from your project.
  3. Verify the removal by running sbt test (or the Maven equivalent) and confirming it succeeds without JaCoCo.

Why: switch from old to new solution is often failing because of sbt or mvn library dependencies. The clean before update is confirmed easy to adopt solution.


2. Add JaCoCo With Method Filtering

Hint: Be ready for customization on your project. Both setup guides are generic.

  1. Follow the JMF plugin setup guide for your build tool:
    sbt: sbt plugin setup docs

    • Add to project/plugins.sbt:
      addSbtPlugin("io.github.moranaapps" % "jacoco-method-filter-sbt" % "2.1.1")
    • Enable the plugin in build.sbt:
      lazy val myModule = (project in file("my-module"))
        .enablePlugins(JacocoFilterPlugin)
    • Optionally add convenience command aliases:
      addCommandAlias("jacoco", "; jacocoOn; clean; test; jacocoReportAll; jacocoOff")
      addCommandAlias("jacocoOn", "; set every jacocoPluginEnabled := true")
      addCommandAlias("jacocoOff", "; set every jacocoPluginEnabled := false")

    Maven: Maven plugin setup docs

    • Add the plugin inside a code-coverage profile in pom.xml (see the full example in the docs).
    • Run: mvn jacoco-method-filter:init-rules to generate the default rules file, then mvn clean verify -Pcode-coverage.
  2. Generate the initial rules file:

    • sbt: sbt jmfInitRules — creates jmf-rules.txt in the project root with sensible Scala defaults.
    • Maven: mvn jacoco-method-filter:init-rules
    • You can also copy and adapt the template from jmf-rules.template.txt or use the balta example as a reference.
  3. (Optional) Enable saving the filter report per module by adding to build.sbt:

    jmfReportFile   := Some(target.value / "jmf-report.json")
    jmfReportFormat := "json"   // or "txt" (default) / "csv"
  4. Run JaCoCo with filtering:

    • sbt: sbt jacoco
    • Maven: mvn clean verify -Pcode-coverage
    • You should see JaCoCo reports generated and, optionally, a filter rules report.
  5. (Optional) Re-apply your backed-up settings from Step 1 (titles, formats, thresholds, exclusions).

  6. Review overridden methods that were filtered by global rules — use include rules to rescue them:

    • Global rules are designed to filter only Scala boilerplate (e.g., copy, equals, hashCode, apply, etc.).
    • If project-defined overrides of these methods were also filtered, that is unintended and must be corrected with include rules (prefixed with +).
    • Run sbt jmfVerify (or mvn compile jacoco-method-filter:verify) to see a dry-run report of excluded and rescued methods without modifying anything.

3. Define Project-Specific Filter Rules

Goal: identify and filter out all methods where unit test coverage either does not make sense or is explicitly excluded by team rules or decisions (e.g., DAOs, trivial delegates, implicit conversions).

  1. (Optional) Temporarily comment out any unmatched global rules shown in the generated report. This reduces noise and makes it easier to focus on writing project-specific rules.

  2. Identify all methods that are valid candidates for exclusion. Common categories include:

    • Deprecated single-call delegates that simply forward to a replacement method
    • One-liner factory wrappers with no logic
    • Trivial implicit conversions
    • Trivial field accessors generated for val members
    • DAO/repository methods excluded by team convention
  3. Add the identified methods to the # PROJECT RULES section of jmf-rules.txt. Use the balta jmf-rules.txt as a reference for rule syntax and documentation conventions.

  4. Re-run code coverage (sbt jacoco / mvn clean verify -Pcode-coverage) after each batch of rule additions. Observe the coverage percentage increasing.

    • Check the unmatched rules section in the report to detect rules that did not match any method — these may have a typo or may be redundant.

4. Set Up the JaCoCo GitHub Actions Workflow (if applicable)

Skip this section if you are not using GitHub Actions for coverage reporting, or if you do not want to use the MoranaApps/jacoco-report action.

Even though this part is independent of the project-level setup, migration to the MoranaApps/jacoco-report GitHub Action is strongly recommended. It automates publishing JaCoCo coverage reports as PR comments with threshold enforcement.

Use the Unify jacoco_report.yml as a reference workflow. This example show the advance usage of this gh action - it uses report-groups instead of global paths.

  • The proposed solution can be still in unmerged PR.
env:
  # hint: "group thresholds" are in format: 'overall*changed-files-average*per-changed-file'
  REPORT_GROUPS: |
    - name: bootstrap
      paths:
        - bootstrap/**/target/jacoco.xml
      thresholds: '9*80*60'
    - name: data-access-spring-web
      paths:
        - contexts/data-access/spring-web/**/target/jacoco.xml
      thresholds: '0*80*60'
    - .... _shortened_: there is more groups

- name: Add JaCoCo Report in PR comments
        uses: MoranaApps/jacoco-report@f9dc5ba989ff2b987b4c2db99217b982c466cca8
        with:
          token: '${{ secrets.GITHUB_TOKEN }}'
          global-thresholds: '43*80'
          report-thresholds-default: '0*0*0'
          skip-unchanged: 'true'
          evaluate-unchanged: 'false'
          report-groups: ${{ env.REPORT_GROUPS }}

For full configuration options and examples, see the jacoco-report README.


5. Enrich a .github/copilot-instructions.md File (Recommended)

This step is optional but strongly recommended. A Copilot instructions file teaches GitHub Copilot the project's coverage conventions so that AI-assisted code suggestions are consistent with your JMF setup from the start — preventing both missing filter rules and incorrectly filtered methods.

You can inspire from the balta .github/copilot-instructions.md at Coverage filtering (JMF) section and a Tooling note. Suggested content:

Tooling
- Compiler warnings treated as errors where configured; coverage ≥ 80% via JMF-enabled JaCoCo (excluding methods listed in `jmf-rules.txt`).

Coverage filtering (JMF)
- Reference: [jacoco-method-filter](https://github.com/MoranaApps/jacoco-method-filter), rules file: `jmf-rules.txt`.

- When a unit test adds value — write one:
  - The method has any logic of its own: branching (`if`/`match`), exception handling, non-trivial transformation, config/resource read, or reflection.

- When to add to `jmf-rules.txt` instead of writing a unit test:
  - The body is a single call with no own logic: it forwards to another overload, calls its non-deprecated replacement, returns a field, or wraps a constructor with no transformation.
  - Litmus test: "Does this method have any logic of its own?" — No → add a JMF rule instead of a test.

- Global rule collision check (CRITICAL):
  - When adding any new method, check whether its name matches a pattern in the `# GLOBAL RULES` section of `jmf-rules.txt`.
  - If a method name matches a global rule AND the method contains domain logic: immediately add an INCLUDE rescue rule (`+FQCN#method(*)`) in the `# INCLUDE RULES` section of `jmf-rules.txt`.
  - High-risk method names (most common collisions): `apply()`, `toString()`, `equals()`, `copy()`, `name()`, `groups()`, `optionalAttributes()`. See the `# GLOBAL RULES` section of `jmf-rules.txt` for the full list.
  - Rationale: broad global rules are designed for compiler-generated boilerplate and can silently suppress coverage for domain methods. INCLUDE rules rescue specific methods from broad exclusions.
  - Example: if adding `def apply(id: String): Record`, add `+*Record$#apply(*)  id:keep-record-factory` to the `# INCLUDE RULES` section to rescue it from the `*$*#apply(*)` global rule.

- JMF drift check (review rule):
  - When modifying a method that already appears in `jmf-rules.txt`, verify its body still qualifies for exclusion.
  - If own logic has been added since the rule was created, remove the JMF rule and write a unit test instead.

- Can NOT add JMF rules for methods with branching logic, error handling, or non-trivial transformations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions