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.

The model SonarQube expects
Across Java, JavaScript, and C++, the workflow stays the same even though the report formats differ:
- Tests run in the build job
- A language-specific tool writes a coverage report
- The Sonar scanner imports that report during analysis
- 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
.execoutput - JavaScript or TypeScript often imports
lcov.info - C and C++ setups often depend on
gcovorlcovoutput, 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.

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.

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.

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
.execfile. 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