Build Better Java Code: Linters, Mutation Testing, and Smarter Code Coverage

FullStackJavaFullStackJava
3 min read

When writing maintainable, robust Java code, unit tests are just the beginning. To truly raise the bar, engineering teams need to analyze their code, not just test it. In this blog, we'll dive into three vital components of a modern Java project's quality toolchain:

  1. Linters (with a focus on Sonar)

  2. Mutation Testing using PITest

  3. Mastering Code Coverage – and why 100% coverage isn't the holy grail


✅ 1. Use a Linter – Sonar is a Solid Standard

A linter is like a no-nonsense teammate who’s never afraid to tell you your code smells.

Why it matters: Linters enforce coding standards, highlight potential bugs, and suggest improvements. While IDEs like IntelliJ do a decent job of catching basic issues, linters bring consistency across teams and CI pipelines.

Why Sonar stands out:

  • SonarQube / SonarCloud integrates directly with your build pipeline.

  • It flags:

    • Code smells

    • Vulnerabilities

    • Bugs

    • Code duplications

  • Provides a "Quality Gate" – a set of pass/fail conditions your code must meet before being merged.

Pro Tip:

Set up SonarCloud with GitHub Actions or GitLab CI to scan every PR. No code merges without passing the quality gate. It’s annoying—but it keeps your codebase clean.


🧪 2. Add Mutation Testing via PITest

“My code has 100% test coverage!”

Cool. But are your tests actually good?

Enter mutation testing, a brutal yet beautiful way to measure how sensitive your tests are to change.

What is Mutation Testing?

Mutation testing works by making small modifications (mutations) to your code—like changing == to !=, or flipping true to false—and checking if your tests fail.

If they don’t, your tests missed a bug. That’s a red flag.

Why PITest?

PITest is the most popular mutation testing tool for Java.

Features:

  • Integrates with Maven, Gradle, and JUnit.

  • Highly configurable – target specific packages or classes.

  • Generates HTML reports with visual mutation results.

Real-World Example:

public boolean isAdult(int age) {
    return age >= 18;
}

PITest may change this to:

return age > 18;

If your test doesn’t catch this subtle change, it’s not robust enough.

🧠 Mutation Score = Killed Mutants / Total Mutants
Aim for 70–80% and improve from there.


📈 3. Understand Your Code Coverage Tool (And Stop Worshiping 100%)

Code coverage tells you what percentage of your codebase is being exercised by tests.

Most Java projects use:

  • JaCoCo (via Maven or Gradle)

  • SonarQube (reads JaCoCo reports)

  • IntelliJ coverage tools

But here's the kicker:

100% coverage ≠ 100% tested

Why?

  • Not all code needs to be tested.

  • Not all code can be meaningfully tested.

Example: Configuration and Boilerplate Code

Let’s say you have a Spring Boot AppConfig class:

@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

Should you write a test just to call restTemplate() and bump coverage? Nope.

Solution:

Sequester untestable or unnecessary code into separate packages, and exclude those packages from coverage reports using configuration in jacoco.gradle or pom.xml.

JaCoCo Exclusion Example (Maven):

<excludes>
    <exclude>**/config/**</exclude>
    <exclude>**/dto/**</exclude>
</excludes>

That way, your 100% coverage target only applies to logic that should be tested.


🛠️ How to Set It All Up Together

For Maven Projects:

  • Sonar: Add sonar-maven-plugin, connect to SonarQube/SonarCloud.

  • PITest: Use the pitest-maven plugin.

  • JaCoCo: Enable with jacoco-maven-plugin.

Integrate them into your pipeline like this:

# GitHub Actions sample
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build and Test
        run: mvn clean verify
      - name: Run Sonar
        run: mvn sonar:sonar
      - name: Run Mutation Tests
        run: mvn org.pitest:pitest-maven:mutationCoverage

💡 Final Thoughts

  • Lint to catch bad patterns.

  • Mutate to find weak tests.

  • Cover wisely, not obsessively.

Together, these practices create a robust quality gate around your codebase, giving you and your team confidence to refactor, ship, and scale faster.

0
Subscribe to my newsletter

Read articles from FullStackJava directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

FullStackJava
FullStackJava

hey i am java backend developer and i have 3 years of experience working as java developer.