SonarQube Code Coverage Integration: Jest, JaCoCo, pytest-cov, and More
SonarQube doesn't run your tests — it reads coverage reports that your test runner produces. To see coverage in SonarQube, you need two things: a test run that generates a coverage file (LCOV, JaCoCo XML, Cobertura XML, etc.) and a scanner configuration that tells SonarQube where to find it. This guide covers the configuration for the most common language/test runner combinations.
How SonarQube Coverage Works
The flow is always the same:
- Run your tests with coverage enabled → test runner writes a coverage report file
- Run sonar-scanner → scanner reads the report and sends it to SonarQube
- SonarQube displays coverage metrics and highlights uncovered lines
SonarQube supports several coverage report formats:
- LCOV — for JavaScript/TypeScript (Jest, Istanbul, Vitest)
- JaCoCo XML — for Java
- Cobertura XML — for Python, .NET, C/C++
- Generic coverage format — SonarQube's own XML format
- OpenCover — for .NET
JavaScript / TypeScript with Jest
Step 1: Configure Jest to produce LCOV
// jest.config.js or package.json
{
"jest": {
"collectCoverage": true,
"coverageReporters": ["lcov", "text"],
"coverageDirectory": "coverage"
}
}Or pass flags on the CLI:
npx jest --coverage --coverageReporters=lcovThis writes coverage/lcov.info.
Step 2: Configure sonar-project.properties
sonar.projectKey=my-js-project
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.ts,**/*.test.js,**/*.spec.ts,**/*.spec.js
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.lcov.reportPaths=coverage/lcov.infoFull CI example (GitHub Actions)
- name: Run tests with coverage
run: npx jest --coverage --coverageReporters=lcov
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
with:
args: >
-Dsonar.projectKey=my-js-project
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}TypeScript with Vitest
Vitest uses V8 or Istanbul for coverage:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['lcov', 'text'],
reportsDirectory: './coverage'
}
}
})Run:
npx vitest run --coverageThen configure SonarQube the same way as Jest:
sonar.javascript.lcov.reportPaths=coverage/lcov.infoJava with JaCoCo
Step 1: Add JaCoCo plugin to Maven
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>prepare-agent</id>
<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 tests:
mvn testThis generates target/site/jacoco/jacoco.xml.
Step 2: Configure SonarQube scanner
sonar.projectKey=my-java-project
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xmlGradle with JaCoCo
// build.gradle
plugins {
id 'jacoco'
id "org.sonarqube" version "4.4.1.3373"
}
jacoco {
toolVersion = "0.8.11"
}
test {
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
}
}
sonarqube {
properties {
property "sonar.projectKey", "my-java-project"
property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml"
}
}Run:
./gradlew test sonarqubePython with pytest-cov
Step 1: Generate coverage in Cobertura XML format
pip install pytest-cov
pytest --cov=src \
--cov-report=xml:coverage.xml \
--cov-report=term-missingStep 2: Configure SonarQube
sonar.projectKey=my-python-project
sonar.sources=src
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xmlFull GitHub Actions example
- name: Run tests
run: |
pip install pytest pytest-cov
pytest --cov=src --cov-report=xml:coverage.xml
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
with:
args: >
-Dsonar.projectKey=my-python-project
-Dsonar.python.coverage.reportPaths=coverage.xml
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}Go with Built-in Coverage
Go has native coverage support:
go test ./... -coverprofile=coverage.outSonarQube reads Go coverage files directly:
sonar.projectKey=my-go-project
sonar.sources=.
sonar.exclusions=**/*_test.go
sonar.tests=.
sonar.test.inclusions=**/*_test.go
sonar.go.coverage.reportPaths=coverage.out.NET with Coverlet
dotnet add package coverlet.msbuild
# Run tests with coverage
dotnet test /p:CollectCoverage=true \
/p:CoverletOutputFormat=opencover \
/p:CoverletOutput=./coverage.opencover.xmlsonar.projectKey=my-dotnet-project
sonar.cs.opencover.reportsPaths=coverage.opencover.xmlMulti-Module Projects
For monorepos or multi-module Maven builds, collect all coverage reports:
Maven multi-module:
<!-- In parent pom.xml, aggregate coverage -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>report-aggregate</id>
<phase>verify</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
</execution>
</executions>
</plugin># Point to the aggregate report
sonar.coverage.jacoco.xmlReportPaths=coverage-report/target/site/jacoco-aggregate/jacoco.xmlJavaScript monorepo (multiple packages):
# Comma-separated list of paths
sonar.javascript.lcov.reportPaths=packages/core/coverage/lcov.info,packages/api/coverage/lcov.info,packages/ui/coverage/lcov.infoDiagnosing Coverage Not Showing Up
Problem: Coverage shows 0% or N/A
Check the SonarQube analysis logs (visible on the Analysis Activity page). Look for:
WARN: No coverage information will be saved because all files are excluded from analysis.or
WARN: Could not find the coverage report file: coverage/lcov.infoCommon fixes:
- Wrong path — the path in
sonar.javascript.lcov.reportPathsmust be relative to the project root (where you runsonar-scanner), not the source directory - Coverage not generated — run tests first; confirm the report file exists before running the scanner
- Test files included in sources — if your test files are in
sonar.sources, SonarQube may count test code in coverage calculations, skewing numbers. Usesonar.exclusionsorsonar.test.inclusionsto separate them
Problem: Coverage lower than expected
SonarQube may be counting files that the test runner excludes. Check:
sonar.exclusionsmatches what your test runner excludes (e.g., mock files, generated code,*.config.ts)- Your coverage report includes the right set of files
Problem: "Executable lines" vs "Lines to Cover"
SonarQube counts executable lines (lines that can actually be executed), not all lines. Blank lines, comments, and type-only declarations don't count. This can make your effective coverage percentage look different from what your test runner reports.
Coverage Thresholds in sonar-project.properties
You can set coverage thresholds at the project level (not recommended — use Quality Gates instead):
# These are informational, not blocking
sonar.coverage.minimum=80For blocking behavior, use Quality Gates (Administration → Quality Gates) instead — they integrate with CI/CD properly.
Best Practices
Always run tests in CI before scanning — the coverage file must exist when the scanner runs. Put the test step before the scan step in your pipeline.
Don't exclude too much — excluding node_modules, dist, and auto-generated files is correct. Excluding actual business logic from coverage is hiding problems.
Use branch coverage in addition to line coverage — line coverage tells you a line ran; branch coverage tells you all logical paths (if/else, ternary, etc.) were tested. Enable both in your test runner:
// Jest
{
"coverageThreshold": {
"global": {
"lines": 80,
"branches": 75
}
}
}Track coverage trends — SonarQube's project history shows coverage over time. A coverage graph trending downward is an early warning that the team is shipping faster than they're testing.