Gradle Source Sets, JVM Test Suites, Test Filtering, Reporting, and Integration Testing: Concepts, Architecture, and Mental Model
Model JVM verification as named source/classpath/task/report evidence so a green build says exactly which unit and integration tests ran.
Learning objectives
- Map production and test source sets to their compile/runtime configurations, tasks, outputs, and report directories.
- Explain the current JVM Test Suite model and why its API remains incubating in Gradle 9.7.1.
-
Distinguish the built-in
testsuite from additional suites, especially production-code visibility and lifecycle wiring. -
Separate class/method filtering (
--tests) from JUnit Platform tag filtering. - Explain why test counts, XML/HTML results, and task graph membership are stronger CI evidence than a generic green build.
- Inspect test topology read-only before changing it.
1. The practical problem: “BUILD SUCCESSFUL” is not a test inventory
Chapter 21 made build and project boundaries explicit. Testing has
the same problem at a smaller scale. A Java repository may contain
production code, fast unit tests, slower integration tests,
generated test fixtures, different runtime dependencies, multiple
Test tasks, and CI filters. If those are collapsed into
the phrase “the tests passed,” a pipeline can become green while an
entire suite never ran.
Gradle models testing through source sets, dependency
configurations, test suites, target Test tasks, and
verification lifecycle edges. Each layer answers a different
question: what is compiled?,
what is on the classpath?, which task executes?,
which tests did that task select?, and
what evidence did it write?
Chapter invariant: a verification claim must identify the suite/task, selected tests, result count, report location, and lifecycle edge that caused the suite to run.
2. Current baseline and API status
The chapter uses Gradle 9.7.1, JDK 21 to run
Gradle, Java 17 for compiled project code, and JUnit
6.1.3. JUnit 6 requires Java 17+ at runtime. The
jvm-test-suite plugin is applied automatically by the
Java plugin, but the JvmTestSuite API remains
incubating in Gradle 9.7.1. That status matters:
the suite model is the current recommended high-level path for the
lab, while organizations that prohibit incubating APIs can use the
manual source-set/Test-task fallback documented by
Gradle.
| Object | Owns | Typical evidence |
|---|---|---|
main source set
|
Production Java/resources plus production compile/runtime configurations. |
src/main/java, src/main/resources,
classes, production outputs.
|
Built-in test suite/source set
|
Unit-test sources,
testImplementation/testRuntimeOnly, and the
test task.
|
src/test/java,
build/test-results/test,
build/reports/tests/test.
|
Additional integrationTest suite
|
Its own SourceSet, dependency configurations, target, and
synthesized Test task.
|
src/integrationTest/java,
integrationTestImplementation, separate
XML/HTML reports.
|
Test task
|
Forked test JVM execution, filters, framework options, logging, reports, parallel forks. | Task outcome, test count, XML files, HTML report, console failures. |
check lifecycle task
|
Verification graph edge, not test discovery by itself. | Selected task graph; must explicitly depend on custom suite targets. |
| Gradle User Home | Wrapper distributions, dependency caches, daemon state, other user-scoped Gradle data. | Isolate in labs; never equate cache warmth with test coverage. |
3. Source, classpath, execution, and evidence are different graphs
flowchart TD M["src/main/java + resources"] --> MO["main output"] U["src/test/java"] --> UC["testImplementation / testRuntimeOnly"] I["src/integrationTest/java"] --> IC["integrationTestImplementation / runtimeOnly"] MO --> UCT["test compile/runtime classpath"] MO -. "explicit project()" .-> ICT["integration compile/runtime classpath"] UC --> UCT IC --> ICT UCT --> T["test task"] ICT --> IT["integrationTest task"] T --> UR["unit XML + HTML"] IT --> IR["integration XML + HTML"] CHECK["check"] --> T CHECK -. "explicit dependsOn" .-> IT
The solid production-output arrow to the built-in unit-test
classpath is a Java-plugin convention. The dotted arrow to the
custom integration suite is deliberately different: current Gradle
documentation states that additional suites must explicitly add
implementation(project()) if they need the current
project’s outputs and relevant exposed dependencies. Likewise,
custom suite targets have no automatic relationship to
check; the build author adds that lifecycle edge.
The final arrows matter operationally. A test task can execute and still be invisible to CI if its XML path is not collected. Conversely, a CI job can upload old files if the workspace is not clean. Evidence is the combination of selected graph, fresh task outcome, and fresh report artifacts.
4. Source sets are compilation/classpath models
The Java plugin creates main and
test source sets. A source set names source directories
and contributes configurations and tasks used to compile/process
that source. The built-in test source set is associated
with configurations such as testImplementation,
testCompileOnly, and testRuntimeOnly, plus
tasks such as compileTestJava,
testClasses, and test.
A source set alone is not proof that tests execute. Gradle’s manual
integration-test documentation is explicit: if you create a source
set manually, you still need its classpaths, a
Test task, and usually a check dependency.
The JVM Test Suite model packages those related pieces into a
higher-level test-suite abstraction.
| Layer | Question to ask | Typical mistake |
|---|---|---|
| Source directories | Which source/resource files belong to this test type? |
Putting slow integration tests under
src/test and later trying to infer them by
naming only.
|
| Configurations | Which libraries/project outputs are available at compile and runtime? | Assuming a custom suite automatically sees production output. |
| Test task | Which compiled tests execute in a forked JVM? | Creating a source set but no execution task. |
| Lifecycle | Which verification command schedules the task? |
Assuming check discovers every custom
Test task.
|
| Reports | Which fresh machine-readable/human-readable evidence exists? | Treating console “success” as equivalent to test counts/results. |
5. JVM Test Suite: one named verification component
A JvmTestSuite currently owns a source set,
suite-scoped dependencies, one or more targets, a testing framework,
and target Test tasks. In Gradle 9.7.1 each suite
commonly has a single target, giving a practical 1:1:1 relationship
between suite, target, and test task. The built-in suite is named
test; declaring
integrationTest synthesizes matching
source/configuration/task names.
testing {
suites {
named<JvmTestSuite>("test") {
useJUnitJupiter("6.1.3")
}
register<JvmTestSuite>("integrationTest") {
useJUnitJupiter("6.1.3")
dependencies {
implementation(project())
}
}
}
}
JvmTestSuite.useJUnitJupiter(version) does more than
Test.useJUnitPlatform(): it adds the framework
dependencies to the suite and configures its test target. Pinning
6.1.3 avoids relying on Gradle’s historical framework default and
makes the learning baseline reviewable.
6. Test selection: task filter versus framework tag
Gradle’s preferred ad-hoc filtering mechanism is
--tests, which matches test class/method names. It
changes what an existing Test task selects; it does not
create a new source set or change dependencies. JUnit tags are
framework metadata. They are applied with @Tag and
selected through JUnit Platform options on the task.
# Class/method filter on the Gradle Test task.
./gradlew test --tests 'GreetingServiceTest.greetsNamedUser'
# A separate suite can also be filtered by name.
./gradlew integrationTest --tests '*GreetingResourceIT*'
targets {
all {
testTask.configure {
useJUnitPlatform {
includeTags("integration")
}
}
}
}
These mechanisms solve different problems. --tests is
excellent for developer focus. A persistent CI policy such as
“integration suite executes only tests explicitly tagged
integration” belongs in version-controlled task/suite configuration.
Gradle’s TestFilter defaults to failing when an
explicit filter matches no tests; Gradle 9.7.1 also has
failOnNoDiscoveredTests, which defaults true when test
sources exist but the framework discovers no tests. Keep those two
“zero tests” cases conceptually separate.
7. Reports are part of the verification contract
Each Test task produces machine-oriented JUnit XML and
a human-oriented HTML report by default. For the conventional unit
task, expect build/test-results/test/ and
build/reports/tests/test/. A task named
integrationTest gets its own corresponding directories.
Gradle also exposes binary test-result variants used by report
aggregation.
CI should archive XML after the task runs and should distinguish suite paths. A useful gate records at least task name, number of tests, failures/skips, and report files. A zero-test XML set is not equivalent to a passing suite.
Gradle 9.7 note: framework-initialization failures for JUnit Platform, JUnit 4, and TestNG are now surfaced in console output even when default test-log granularity would previously have hidden the underlying startup failure. The XML/HTML report remains important evidence.
8. Read-only inspection before mutation
Start with identity, then ask Gradle which test-related objects already exist. These commands mutate ordinary diagnostic/cache state but should not change source configuration.
java -version
./gradlew --version
./gradlew projects
./gradlew tasks --group verification
./gradlew help --task test
./gradlew dependencies --configuration testRuntimeClasspath
./gradlew properties | grep -E '^(group|version):' || true
find build/test-results build/reports/tests -type f -print 2>/dev/null | sort || true
Do not infer integration coverage from the presence of a directory from a prior run. Compare timestamps or start from a clean disposable workspace when evidence matters.
9. Trust boundaries in test execution
Tests execute arbitrary code in forked JVMs. They can read environment variables, system properties, local files, network endpoints, and credentials available to the process. A test dependency is also a supply-chain dependency. Therefore a CI test lane should use least-privilege credentials, explicit environment fixtures, controlled repositories, and versioned test dependencies.
Reports can leak secrets if tests print them. Do not solve test diagnosis by turning on unrestricted standard streams in CI. Prefer concise failure evidence and synthetic credentials. Likewise, do not add real database/cloud access merely to demonstrate “integration testing”; this chapter’s mandatory path uses only project-local code/resources.
10. DevOps operating rule: prove the gate, not just the task
A production verification policy can be expressed as a matrix: suite → task → classpath → filter → report → lifecycle gate. If any column is implicit, the risk of false-green CI increases. The next lessons build and break that matrix deliberately.
| Claim | Evidence needed |
|---|---|
| Unit tests passed |
Fresh :test outcome plus unit XML/HTML and
nonzero intended test count.
|
| Integration tests passed |
Fresh :integrationTest outcome plus integration
XML/HTML and expected test inventory.
|
check is the verification gate |
Task graph proves check schedules both intended
suites.
|
| A filter was intentional | Command/configuration is recorded and the selected test inventory is visible. |
| Parallel execution is safe | Tests have isolated mutable state/resources; speedup is measured separately from correctness. |
11. Summary and next bridge
Source sets define source/classpath state; suites group that state
with dependencies/framework/targets; Test tasks execute
selected tests in forked JVMs; reports record evidence; and
lifecycle edges determine what a high-level verification command
actually schedules. Lesson 2 turns this model into a reproducible
local workflow.
Knowledge check
Does declaring integrationTest automatically make
check run it?
No. Additional test-suite targets have no automatic relationship
to check; wire the dependency explicitly.
Does a custom JVM test suite automatically see the current project output?
No. Unlike the built-in test suite, an additional
suite must explicitly add
implementation(project()) when it needs production
output.
What does --tests change?
The selected tests for a Test task; it does not
create a new suite, source set, or classpath.
Why are XML/HTML reports part of the CI contract?
They expose which tests ran and their results; a generic build success does not prove suite coverage.
What is version-sensitive about this chapter?
The JVM Test Suite API remains incubating in Gradle 9.7.1, so its status and fallback path should be documented.
Why isolate test credentials and environment state?
Tests execute code with process access; secret/environment leakage and shared-state coupling are real supply-chain and reliability risks.
Official references and version notes
-
Gradle JVM Test Suite Plugin
— current suite/source-set/task model, additional-suite
dependencies, target tasks,
checkwiring, and outgoing test-result variants. - JvmTestSuite DSL — current API status and framework configuration. The API remains incubating in Gradle 9.7.1.
- Testing in Java & JVM Projects — test execution, filtering, reporting, JUnit Platform, manual integration-test fallback, and troubleshooting.
-
Gradle Test task
— forked test JVMs, reports, filtering,
maxParallelForks, failure behavior, and framework options. - TestFilter — class/method filters and fail-on-no-match behavior.
- Gradle 9.7.1 Release Notes — pinned Gradle baseline and current test-reporting behavior.
- JUnit 6.1.3 Overview — current JUnit Platform/Jupiter composition and Java 17+ runtime baseline.
-
JUnit 6.1.3 Tagging and Filtering
—
@Tagsemantics used in the examples.
Version-sensitive behavior was rechecked against current Gradle and
JUnit primary documentation on 2026-08-24. Mandatory labs use Gradle
9.7.1 through the previously verified Wrapper, JDK 21 as the Gradle
runtime, Java 17 as the project target, JUnit 6.1.3, and an isolated
GRADLE_USER_HOME. The JVM Test Suite API is explicitly
labeled incubating. No hosted CI, database, cloud service,
commercial repository, or paid test platform is required.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.