code coverage sonarqubesonarqubeci/cdjacocosoftware quality

Optimize Code Coverage SonarQube for Quality

Master code coverage sonarqube. This guide covers JaCoCo, Istanbul, lcov, CI/CD integration, quality gates, & troubleshooting 0% coverage.

Published May 18, 2026 · Updated May 18, 2026

Optimize Code Coverage SonarQube for Quality

Your pipeline passed. The tests are green. SonarQube finishes analysis and shows 0.0% coverage.

That result usually sends teams in the wrong direction. They start questioning SonarQube, swapping scanners, or changing quality gates when the actual problem is nearly always earlier in the chain: the report was never generated, never found, or never matched the source layout SonarQube analysed. Public guidance on this problem exists, but it's scattered across docs, blog posts, and walkthroughs, which makes onboarding harder than it should be, especially when your team is already under release pressure based on SonarSource guidance discussed in this walkthrough video.

The fix is to treat code coverage sonarqube as a pipeline, not a toggle. Tests must run. A coverage tool must produce a report. The scanner must import that report. SonarQube must map it back to the exact files in your project. If any one of those steps is off, the dashboard lies to you.

That's also why coverage belongs inside a wider engineering control system, not as a vanity metric. Teams that already think in terms of a secure software development life cycle usually adopt SonarQube more successfully, because they use coverage to reduce regression risk and improve decision-making, not to chase a pretty badge.

Introduction From Zero to Hero with SonarQube Coverage

The most common SonarQube coverage failure doesn't look like a failure at first. Your build succeeds, the scanner exits cleanly, and the dashboard still says 0%. That disconnect is why this topic frustrates even experienced engineers.

In practice, coverage setup breaks in boring places. A report lands in a different folder in CI than it does locally. The scanner runs before the tests. A JavaScript job creates lcov.info, but SonarQube is looking for a JaCoCo XML path. None of that is glamorous, but that's where most real fixes happen.

Why teams get stuck

Coverage setup crosses tool boundaries. Your test runner, build tool, coverage reporter, scanner, and SonarQube server each do a separate job. If one part is misaligned, the whole chain fails without warning.

Practical rule: If SonarQube shows no coverage, start by assuming the import path is wrong before assuming the product is broken.

That mindset saves time. SonarQube is usually doing exactly what you told it to do. The problem is that many pipelines tell it the wrong thing.

What good coverage setup looks like

A solid setup has three traits:

  • The report is explicit: Your build creates a known file in a known format.
  • The scanner is explicit: The analysis points to that file using the correct property.
  • The team is realistic: Coverage is used to highlight risk in important code paths, not to reward test inflation.

That last part matters. High coverage can still hide weak tests if those tests execute code without checking behaviour. SonarQube is useful when it helps you see what wasn't exercised and where your risk sits, not when it pressures the team into gaming the number.

Understanding SonarQube's Approach to Code Coverage

Teams usually notice SonarQube coverage for the first time when the scan finishes cleanly and the dashboard still shows 0%. That result feels like a scanner failure, but in practice it usually means SonarQube completed analysis without importing a usable report.

SonarQube reads coverage data. It does not create it. Your test and coverage tools produce the raw report. SonarQube ingests that report, maps it to the indexed source files, and shows the result in a format your team can review in pull requests, project dashboards, and quality gates.

A diagram illustrating the three-step process of how SonarQube processes code coverage data from tests.

The model SonarQube expects

Across Java, JavaScript, and C++, the workflow stays the same even though the report formats differ:

  1. Tests run in the build job
  2. A language-specific tool writes a coverage report
  3. The Sonar scanner imports that report during analysis
  4. SonarQube attaches the imported coverage to the matching files

The sequence matters more than many teams expect. If Jenkins, GitHub Actions, GitLab CI, or Azure DevOps runs the scanner before the test stage finishes, SonarQube has nothing to import. If the report exists but the scanner points to the wrong path, analysis still succeeds and coverage still shows 0%. That is why coverage problems are often pipeline problems, not SonarQube problems.

What SonarQube is actually measuring

SonarQube shows imported coverage against the code it analyzed. That distinction matters.

If the scanner indexes src/main/java but your report was generated from a different checkout path, a container workdir, or a compiled artifact directory, SonarQube may not match the files correctly. The report can be valid and still appear empty in the UI. I see this often in monorepos where one job produces lcov.info in a subfolder, while the scanner runs from the repository root with a different base directory.

The useful metrics are usually:

  • Line coverage, whether executable lines ran during tests
  • Branch or condition coverage, whether decision paths were exercised
  • Coverage on new code, whether recent changes were tested before merge

Coverage on new code is usually the metric that changes team behavior. It is more realistic than chasing a single project-wide target on a legacy codebase that already shipped with thin tests.

Why 0% coverage happens so often

The common failure mode is simple. The report format and the SonarQube property do not match the stack.

A few examples:

  • Java often imports JaCoCo XML, not the old binary .exec output
  • JavaScript or TypeScript often imports lcov.info
  • C and C++ setups often depend on gcov or lcov output, plus correct path resolution inside the build environment

That is why a multi-stack setup needs more discipline than a single Maven tutorial suggests. One repository can contain a Java service, a React frontend, and a native module, each with different report paths and scanner properties. SonarQube can handle that, but only if the build produces each report consistently and the analysis step points to the right files.

Reading the metric without overrating it

Coverage is useful because it highlights untested code paths. It does not prove the tests are good.

A line can be marked as covered even when the test made no meaningful assertion. Branch coverage usually gives a better signal for risk because it shows whether the alternate paths ran, not just whether execution touched the file once. That matters in validation logic, permission checks, null handling, and error branches. Those are also the places teams care about most when they are already focused on finding code vulnerabilities early.

SonarQube works best when coverage is read next to static analysis results, not in isolation. If your team is still aligning on that distinction, this primer on what static analysis means in practice is a useful reference. Coverage answers, "Did tests execute this path?" Static analysis answers different questions about correctness, maintainability, and security.

Used that way, SonarQube becomes much more than a percentage on a dashboard. It becomes a fast way to spot risky new code, verify whether the right parts of the system were exercised, and diagnose why a pipeline that "passed" still gave you 0% coverage.

Generating Language-Specific Coverage Reports

The scanner can't import what your build never produced. Before you touch SonarQube properties, make sure each stack in your estate emits a coverage file consistently in local development and in CI.

That means different tools for different ecosystems. Java teams usually use JaCoCo. JavaScript and TypeScript teams often use nyc with Jest or Mocha and produce LCOV. C and C++ teams often combine compiler coverage flags with gcov and lcov.

A hand-drawn illustration showing a folder filled with puzzle pieces representing various popular programming languages and code.

Java with Maven and JaCoCo

For Maven, the usual pattern is to attach the JaCoCo agent during test execution and generate the XML report afterwards.

<build>
  <plugins>
    <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>REPLACE_WITH_YOUR_VERSION</version>
      <executions>
        <execution>
          <goals>
            <goal>prepare-agent</goal>
          </goals>
        </execution>
        <execution>
          <id>report</id>
          <phase>test</phase>
          <goals>
            <goal>report</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Run:

mvn clean test

Typical output file:

target/site/jacoco/jacoco.xml

For Gradle, the pattern is similar:

plugins {
  id 'java'
  id 'jacoco'
}

test {
  useJUnitPlatform()
  finalizedBy jacocoTestReport
}

jacocoTestReport {
  reports {
    xml.required = true
    html.required = true
  }
}

Run:

./gradlew clean test jacocoTestReport

Typical output file:

build/reports/jacoco/test/jacocoTestReport.xml

The practical check is simple. Don't move on until that XML file exists where you expect it.

JavaScript and TypeScript with nyc

For Node-based projects, SonarQube usually consumes an LCOV report. One common setup uses Jest with nyc.

Install the tooling:

npm install --save-dev nyc jest

Add scripts:

{
  "scripts": {
    "test": "jest",
    "coverage": "nyc --reporter=lcov --reporter=text npm test"
  }
}

Run:

npm run coverage

Typical output file:

coverage/lcov.info

If you use Mocha instead of Jest, the nyc wrapper stays broadly similar. The important part isn't the test framework. It's that the pipeline generates a stable lcov.info file every time.

If you're also working across backend services, it helps to align test discipline across languages. This practical guide to backend Python unit tests is a useful companion for teams standardising how they write assertions, fixtures, and edge-case tests across mixed stacks.

C and C++ with gcov and lcov

Native codebases take more care because the compiler flags matter. You need to compile with coverage instrumentation, run the tests, then harvest the result into an LCOV-style report.

A common flow looks like this:

cmake -DCMAKE_BUILD_TYPE=Debug .
make
ctest
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage-html

Depending on your build toolchain, you may also need compiler and linker coverage flags enabled in your CMake or Make configuration. The exact syntax varies by environment, but the pattern doesn't: instrument, test, collect, then export.

What works and what doesn't

I've seen teams lose hours by trying to solve SonarQube coverage from the scanner backwards. That rarely helps. Start with the artefact.

Use this checklist before touching any Sonar property:

  • Check the file exists: Open the generated report in CI, not just locally.
  • Check the format is right: XML for JaCoCo is not the same as LCOV.
  • Check the file has real paths: Coverage reports with paths that don't match the analysed workspace will import badly or not at all.
  • Check the run order: Tests must complete before report generation and analysis.

If the coverage file isn't present in the workspace at scan time, SonarQube can't rescue the build for you.

Connecting Coverage Reports to SonarQube Analysis

Once the report exists, the next job is telling SonarQube exactly where to find it. Many pipelines fail at this stage. The report is there, but the scanner points to the wrong property, the wrong path, or a path that only exists on a developer laptop.

You can set these properties in a sonar-project.properties file, in build tool configuration, or as command-line arguments. I prefer keeping them close to the project so they're versioned and visible in code review.

Property mapping that usually matters

The key is to map the report format to the right SonarQube property.

| Language / Tool | Report Format | SonarQube Property | |---|---|---| | Java with JaCoCo | XML | sonar.coverage.jacoco.xmlReportPaths | | JavaScript or TypeScript with nyc or Istanbul | LCOV | sonar.javascript.lcov.reportPaths | | C or C++ with lcov | LCOV | sonar.cfamily.gcov.reportsPath or project-specific C/C++ coverage settings depending on your setup |

For Java, a simple properties file often looks like this:

sonar.projectKey=my-java-app
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml

For JavaScript:

sonar.projectKey=my-js-app
sonar.sources=src
sonar.tests=test
sonar.javascript.lcov.reportPaths=coverage/lcov.info

For mixed repositories, keep the paths relative to the scan root whenever possible. Relative paths are more portable across local development, CI runners, and containerised jobs.

Path mistakes that break imports

The scanner is literal. It won't guess what you meant.

The failures I see most often are these:

  • Wrong working directory: The scanner runs from a subfolder, but the report path assumes repo root.
  • Wrong property name: Teams use a Java property for a JavaScript report or vice versa.
  • Generated file not persisted: One CI step creates the report, another isolated step runs analysis without the artefact.
  • Path mismatch inside the report: The report references files using paths that don't match the source tree SonarQube analysed.

A better way to validate

Before the Sonar scan, add one small verification step to the pipeline:

ls -la coverage
test -f coverage/lcov.info

Or for Java:

ls -la target/site/jacoco
test -f target/site/jacoco/jacoco.xml

That tiny check catches a surprising number of bad assumptions. If the file isn't there before scanning, the scanner property doesn't matter.

Field note: Most “SonarQube coverage bugs” turn out to be workspace, path, or report-format bugs.

Integrating Coverage Quality Gates into CI/CD Pipelines

A team gets coverage importing correctly on a laptop, merges the SonarQube config, and then pull requests still pass with no useful guardrail. The missing step is usually pipeline enforcement. SonarQube becomes useful once coverage is checked on every change and the build can fail when new code drops below the standard your team agreed to.

Treat the gate as a release control, not a vanity metric. Coverage helps catch untested paths, but it does not compensate for weak assertions, skipped integration tests, or poor branch coverage. Set the rule so it changes day-to-day behavior. Do not set it so high that people start writing tests only to satisfy a number.

A hand-drawn illustration showing a software pipeline processing code, build, and test steps into a final result.

GitHub Actions example

In GitHub Actions, keep test execution, coverage generation, and analysis in one job unless you have a clear reason to split them. That avoids the common case where the scan runs in a fresh workspace and imports nothing.

name: sonar

on:
  push:
    branches: [main]
  pull_request:

jobs:
  analyse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'

      - name: Build and test
        run: mvn clean test

      - name: Verify JaCoCo report
        run: test -f target/site/jacoco/jacoco.xml

      - name: Sonar scan
        run: mvn sonar:sonar -Dsonar.token=${{ secrets.SONAR_TOKEN }}
        env:
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

For a JavaScript project, the pattern stays the same. Run npm ci, run the test command that writes coverage/lcov.info, verify the file exists, then scan. For C++ builds, the same sequencing applies, but the report format and generation tooling differ, so it is worth validating the exported file before analysis just as strictly.

GitLab CI example

GitLab CI works well for coverage gates if you are deliberate about artifacts. A separate test stage and analyse stage is fine. Forgetting to pass the report forward is what produces the familiar SonarQube analysis with 0% coverage.

stages:
  - test
  - analyse

test:
  stage: test
  image: maven:3-eclipse-temurin-17
  script:
    - mvn clean test
    - test -f target/site/jacoco/jacoco.xml
  artifacts:
    paths:
      - target/site/jacoco/jacoco.xml

sonarqube:
  stage: analyse
  image: maven:3-eclipse-temurin-17
  dependencies:
    - test
  script:
    - mvn sonar:sonar -Dsonar.token=$SONAR_TOKEN

If you use child pipelines, Docker-in-Docker, or separate runners for test and analysis, check artifact retention and working directory assumptions first. Those details cause more failed imports than SonarQube itself.

Jenkins example

Jenkins adds another variable. Agents often do not share the same filesystem, even when the pipeline looks linear in the UI.

pipeline {
  agent any

  stages {
    stage('Build and Test') {
      steps {
        sh 'mvn clean test'
        sh 'test -f target/site/jacoco/jacoco.xml'
      }
    }

    stage('SonarQube Analysis') {
      steps {
        withSonarQubeEnv('SonarQube') {
          sh 'mvn sonar:sonar'
        }
      }
    }
  }
}

If the test stage runs on one agent and analysis runs on another, stash or archive the coverage report and restore it before scanning. I have seen teams spend hours checking Sonar properties when the actual problem was a workspace that no longer contained the XML file.

Set the gate on new code first

For a first rollout, use a quality gate that checks coverage on new code rather than forcing a repo-wide threshold across an old codebase. That gives developers a target they can meet in pull requests, and it stops the long decline where every sprint adds more untested code.

This matters even more in mixed-stack repositories. A Java service, a Node frontend, and a C++ module rarely mature at the same pace, and the test depth is often different for good reasons. New-code gates let you improve all three without pretending they should start from the same baseline.

A practical policy looks like this:

  • Require coverage on new code, not only overall coverage.
  • Keep bugs, vulnerabilities, and hotspots in the same gate discussion.
  • Fail the pipeline only after tests and report generation complete successfully.
  • Review unexpected drops manually before changing thresholds.

If your team is tightening release controls, SonarQube should sit beside the rest of your CI/CD security checks and deployment safeguards. Coverage is one signal in the pipeline. It should influence merge decisions, but it should not be the only one.

Speed still matters. If the gate takes too long, developers will stop trusting it and start looking for bypasses. Teams working on build time and test feedback loops can borrow ideas from how GoReplay optimizes CI/CD, especially when the goal is to keep quality checks strict without slowing every pull request to a crawl.

Expert Answers to Frequent SonarQube Coverage Questions

A team usually asks coverage questions after the first frustrating pipeline run. Tests passed, the scanner finished, and SonarQube still shows 0%, or the percentage looks nothing like the local report. That is the point where setup details matter more than the headline number.

An infographic titled SonarQube Coverage FAQ providing answers to three common questions about code coverage metrics.

Why is SonarQube showing 0% coverage

In practice, 0% almost always means SonarQube never imported a usable report. The failure is usually in the handoff between the test tool and the scanner, not in SonarQube itself.

Check these in order:

  • The report was generated. In Java, confirm JaCoCo produced XML, not only the .exec file. In JavaScript and TypeScript, confirm LCOV exists. In C and C++, confirm the build produced the coverage artifacts your reporting tool expects.
  • The scanner points to the right file. A valid property with the wrong path still gives you 0%.
  • The report format matches the property. SonarQube imports specific formats. XML, LCOV, and generic coverage are not interchangeable.
  • Tests run before analysis. If the SonarScanner step starts before coverage files exist, the import is empty.
  • Paths inside the report match the checked-out source tree. This breaks often in containers, monorepos, and CI jobs that change the working directory.

I usually start by opening the report file in the CI workspace and then reading the scanner log line by line. If the log says the report is missing, ignore the dashboard and fix the pipeline. If the log says the report was found but no files matched, fix path mapping.

This shows up differently across stacks. A Maven job may succeed while JaCoCo XML generation is skipped. A Jest job may write coverage/lcov.info locally but place it in a different folder in GitHub Actions. A C++ pipeline may collect gcov data on one runner and run SonarScanner on another runner with no artifacts copied across. Different tools, same symptom.

Why do local numbers and SonarQube numbers differ

Different tools count coverable lines differently, and they may not analyze the exact same files. That alone is enough to create a noticeable gap.

The common causes are straightforward:

  • Local tools may include files that SonarQube excludes.
  • SonarQube may ignore generated code or files outside the analysis scope.
  • Branch coverage and line coverage are often reported differently across tools.
  • Monorepos can mix frontend and backend reports in ways that make local summaries look higher than the imported project result.

Treat one reporting chain as the team standard and stick to it. If pull requests are judged in SonarQube, compare against SonarQube. Do not waste review time arguing over a few percentage points from a local HTML report that used different inputs.

A quick check helps here. Compare the file list in the coverage report with the file list SonarQube analyzed. If those two sets do not line up, the percentages will not line up either.

Is 80% coverage a good target

80% is a reasonable starting point for many teams. It is not a rule, and it is a poor target if developers start writing tests that only execute lines without checking behavior.

Set targets based on risk and on how the code is used. For a payment flow, auth layer, or shared parser, I would rather see strong branch coverage and meaningful assertions at 70% than weak line coverage at 90%. For low-risk UI glue code, pushing for a perfect number can waste time that should go into end-to-end checks or defect prevention elsewhere.

The better question is whether the current threshold catches regressions in changed code. If it does, keep it. If developers keep hitting the number while obvious edge cases slip through, the metric is too shallow to guide release decisions.

How should we handle large legacy codebases

Start with new code and recently changed files. That gives the team a standard they can meet without blocking every release on old gaps.

Then work module by module. In mixed-stack repos, pick the places where failures hurt most first. A Java service that handles billing, a Node API gateway, and a C++ processing component should not all get the same remediation plan on day one. Stabilize report generation for each stack, make sure SonarQube imports every report consistently, and raise expectations where the code changes most often.

If you want the same kind of fast feedback for security that SonarQube gives you for code quality, AuditYour.App is worth a look. It scans Supabase, Firebase, websites, and mobile apps for exposed rules, leaked secrets, unprotected RPCs, and other high-risk misconfigurations, with no heavy setup. It fits well beside CI/CD quality gates when you want one place to catch the issues your tests and coverage reports won't.

Scan your app for this vulnerability

AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.

Run Free Scan
Optimize Code Coverage SonarQube for Quality | AuditYourApp