Chapter 22Lesson 01~215 minutes

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.

Gradle 9.7.1Source setsJVM Test SuiteTest graphCI evidence

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 test suite 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

Test evidence flow
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?

Does a custom JVM test suite automatically see the current project output?

What does --tests change?

Why are XML/HTML reports part of the CI contract?

What is version-sensitive about this chapter?

Why isolate test credentials and environment state?

Official references and version notes

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.