Support for Gradle testing suites

For me at least, it’s often not only the test task that exists as a Test task in Gradle. For example, I often add the integrationTest task through the experimental The JVM Test Suite Plugin , and even if it’s unstable API, we have been creating test tasks before in other ways, e.g., before the new official API we used the gradle-testsets-plugin. The point is, it would be nice if the plugin allowed us an easy way to pick up the sources and binaries of every test task, not just the ones of test.

Thanks in advance.

Currently this is the workaround that I use, which seems to work well:

sonar {
	properties {
		val testTasks = tasks.withType<Test>().map { it.name }

		property("sonar.tests", testTasks.flatMap { sourceSets[it].allSource.srcDirs }.toSet())
		property(
			"sonar.java.test.binaries",
			testTasks.flatMap { sourceSets[it].output.classesDirs.files }.toSet()
		)
		property(
			"sonar.java.test.libraries",
			testTasks.flatMap { sourceSets[it].compileClasspath.files }.toSet()
		)
		property(
			"sonar.junit.reportPaths",
			testTasks.map {
				tasks.named<Test>(it).get().reports.junitXml.outputLocation.get().asFile
			})
	}
}

But as you can see, it’s not ideal, it’s a lot of code and “secret” knowledge of what the plugin does for the test task.

Edit: As for code coverage reporting, you might want something like this, but I have not tested it yet (my setup is different so I didn’t need to):

		property(
			"sonar.coverage.jacoco.xmlReportPaths",
			tasks.withType<JacocoReport>().map {
				it.reports.xml.outputLocation.get().asFile
			}
		)

Same request here, with a data point that I think reframes it from convenience to correctness: the sources are not merely analysed as main code by mistake — they are dropped silently.

Because the scanner derives sonar.tests from the test source set only, files in any other test suite belong to neither sonar.sources nor sonar.tests. They are never indexed, no rule ever runs on them, and nothing in the log says so. You only notice if you go looking.

Versions: SonarScanner for Gradle 7.5.0.8588, Gradle 9.7.1, SonarQube Server 2026.4.1.

Impact in one real multi-module build (24 Gradle projects, integrationTest registered via testing { suites { register<JvmTestSuite>("integrationTest") } }, plus java-test-fixtures):

test directories passed modules including integrationTest/testFixtures sonar.java.test.binaries
plugin default 17 0 17
with workaround 28 9 28

That is 103 Java files — 52 integration tests and 51 test fixtures — absent from analysis. One module consists only of integration tests and test fixtures, so it was invisible almost in its entirety.

Anyone can check their own build without touching SonarQube, which is how the numbers above were produced:

./gradlew sonar -Dsonar.scanner.internal.dumpToFile=/tmp/props.txt
grep 'sonar\.tests' /tmp/props.txt

Suggested detection rule, rather than “all source sets except main”: the JvmTestSuites in the
testing.suites container (first-class since Gradle 7.3, and what integrationTest already is) plus the
testFixtures source set contributed by java-test-fixtures. That keeps jmh, generated or other custom
source sets out, and needs no new configuration from users — the build already declares its intent.

The workaround we settled on (Kotlin DSL, per project, in a convention plugin):

sonar {
    properties {
        val testSourceSets = the<SourceSetContainer>().filter { it.name != "main" }
        // Skip missing directories, the analysis fails on them.
        property("sonar.tests", testSourceSets.flatMap { it.java.srcDirs }.filter { it.isDirectory })
        property("sonar.java.test.binaries",
                 testSourceSets.flatMap { it.output.classesDirs.files }.filter { it.isDirectory })
    }
}

// The class paths the analyzers resolve types against.
// The Sonar plugin derives them from the "main" and "test" source sets only,
// so without this every dependency of the integration tests and the test fixtures is unresolved,
// and Sonar downgrades all rules that need type information.
// The runtime class path adds the transitive "implementation" dependencies:
// Sonar needs the full closure to resolve types a dependency exposes,
// while the compile class path deliberately hides them.
// Resolve via the resolver task, because the Sonar task itself may not resolve other projects' configurations.
val mainClassPath = files(provider {
    the<SourceSetContainer>().filter { it.name == "main" }
            .flatMap { listOf(it.compileClasspath, it.runtimeClasspath) }
})
val testClassPath = files(provider {
    the<SourceSetContainer>().filter { it.name != "main" }
            .flatMap { listOf(it.compileClasspath, it.runtimeClasspath) }
})
tasks.named<SonarResolverTask>(SonarResolverTask.TASK_NAME) {
    setCompileClasspath(provider { mainClassPath })
    setTestCompileClasspath(provider { testClassPath })
}

Two details that cost us time and may help others: a non-existent directory in sonar.tests fails the analysis outright (File system element for parameter … does not exist), hence the isDirectory filter; and main and test sets must stay disjoint or indexing aborts with “can’t be indexed twice”. Unlike the original post we did not need sonar.java.test.libraries, since our integrationTest configurations extend the test ones, so the classpath is already right.

Is this on the roadmap, or tracked in a ticket we could follow?

Hey @rkrisztian and @markusheiden, thanks for bringing this up and sorry for the delay, this post kind of fell through the cracks. I think what you’re asking makes sense, I will flag this for our PMs.