Checkpoint Lab — Gradle Source Sets, JVM Test Suites, Test Filtering, Reporting, and Integration Testing
Create unit and integration suites with distinct source, classpath, task, and report state; disconnect the integration gate, prove the false green, then repair it.
Learning objectives
- Create a complete unit/integration verification dossier with distinct sources, classpaths, tasks, and reports.
- Predict task-graph and report-state changes before disconnecting or reconnecting the integration suite.
-
Demonstrate a false-green
checkby intentionally removing only the lifecycle edge. - Demonstrate one controlled failing integration test and preserve its XML/HTML evidence.
- Repair both lifecycle and test failure without deleting normal Gradle state.
- Document a production-ready mapping from suites to CI evidence and bridge to Java toolchains.
1. Checkpoint scenario and acceptance contract
Build a local Java library with a unit suite and a separate integration suite. The integration suite verifies a production resource. Your final dossier must prove:
- Gradle/JDK/toolchain/test-framework identity.
- Unit and integration source directories/configurations/task names.
- Distinct unit and integration XML/HTML report state.
-
checkschedules both suites in the healthy build. -
Removing only the lifecycle edge creates a reproducible
false-green
check. - A deliberately failing integration assertion produces a nonzero integration/check result with preserved evidence.
- All experimental changes are reverted and only disposable state is deleted.
2. Preflight and pinned assumptions
rm -rf gradle-testing-checkpoint
mkdir gradle-testing-checkpoint
cd gradle-testing-checkpoint
# Copy the previously verified Gradle 9.7.1 Wrapper files here.
export GRADLE_USER_HOME="$PWD/.gradle-user-home"
java -version
./gradlew --version
./gradlew tasks --group verification
Record the Gradle 9.7.1 and JVM 21 lines. The build requests a Java 17 toolchain. JUnit 6.1.3 is pinned in both suites and requires Java 17+. If those assumptions differ, stop and update the dossier instead of silently accepting another runtime.
3. Predict before changing state
| Change | Prediction before execution |
|---|---|
Add integrationTest suite |
New source set/configurations/compile task/test task/report directories appear. |
Add implementation(project()) |
Integration runtime classpath gains production output,
including academy-banner.txt.
|
Add check.dependsOn(integrationTest) |
check --dry-run schedules both
test and integrationTest.
|
Remove only that dependsOn line |
Direct integrationTest still exists, but
check no longer schedules it.
|
| Change expected banner to wrong text | Integration task fails; its XML/HTML records the assertion failure; healthy unit tests remain independent. |
Save this prediction table in your notes. The checkpoint is about verifying the model, not merely ending with green output.
4. Create the healthy baseline
Create the following files.
settings.gradle.kts
rootProject.name = "gradle-testing-lab"
build.gradle.kts
plugins {
`java-library`
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
testing {
suites {
named<JvmTestSuite>("test") {
useJUnitJupiter("6.1.3")
}
register<JvmTestSuite>("integrationTest") {
useJUnitJupiter("6.1.3")
dependencies {
// Additional suites do not automatically see production output.
implementation(project())
}
targets {
all {
testTask.configure {
shouldRunAfter(tasks.named("test"))
useJUnitPlatform {
includeTags("integration")
}
}
}
}
}
}
}
tasks.named("check") {
dependsOn(testing.suites.named("integrationTest"))
}
src/main/java/dev/academy/testing/GreetingService.java
package dev.academy.testing;
public final class GreetingService {
private GreetingService() {}
public static String greeting(String name) {
if (name == null || name.isBlank()) {
return "hello, stranger";
}
return "hello, " + name.strip();
}
}
src/main/resources/academy-banner.txt
academy-integration
src/test/java/dev/academy/testing/GreetingServiceTest.java
package dev.academy.testing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class GreetingServiceTest {
@Test
@Tag("unit")
void greetsNamedUser() {
assertEquals("hello, Ada", GreetingService.greeting("Ada"));
}
@Test
@Tag("unit")
void handlesBlankName() {
assertEquals("hello, stranger", GreetingService.greeting(" "));
}
}
src/integrationTest/java/dev/academy/testing/GreetingResourceIT.java
package dev.academy.testing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("integration")
class GreetingResourceIT {
@Test
void productionResourceIsOnIntegrationRuntimeClasspath() throws IOException {
try (InputStream in = getClass().getResourceAsStream("/academy-banner.txt")) {
assertNotNull(in, "production resource should be visible");
assertEquals("academy-integration", new String(in.readAllBytes(), StandardCharsets.UTF_8).trim());
}
}
}
5. Capture suite/task/classpath/report mapping
./gradlew tasks --group verification > tasks.txt
./gradlew dependencies --configuration testRuntimeClasspath > unit-classpath.txt
./gradlew dependencies --configuration integrationTestRuntimeClasspath > integration-classpath.txt
./gradlew check --dry-run > check-graph.txt
cat check-graph.txt
./gradlew clean check
find build/test-results -type f -name 'TEST-*.xml' -print | sort > reports.txt
printf '%s
' 'build/reports/tests/test/index.html' >> reports.txt
printf '%s
' 'build/reports/tests/integrationTest/index.html' >> reports.txt
cat reports.txt
Verify the integration runtime classpath evidence differs from a
suite with no project dependency and that the clean
check creates both report families. This is the healthy
baseline.
6. Draw the verified topology
flowchart LR
CHECK["check"] --> TEST["test"]
CHECK --> IT["integrationTest"]
MAIN["main output + resources"] --> TESTCP["test runtime classpath"]
MAIN --> ITCP["integration runtime classpath via project()"]
UNIT["src/test/java"] --> TEST
INT["src/integrationTest/java"] --> IT
TEST --> UXML["build/test-results/test"]
IT --> IXML["build/test-results/integrationTest"]
Annotate your own copy with the observed task/report names. This makes the later missing edge obvious.
7. Intentionally disconnect integration tests from
check
Back up the healthy build script, then remove only the final lifecycle block.
cp build.gradle.kts build.gradle.kts.healthy
python - <<'PY2'
from pathlib import Path
p=Path('build.gradle.kts')
s=p.read_text()
block='\ntasks.named("check") {\n dependsOn(testing.suites.named("integrationTest"))\n}\n'
if block not in s:
raise SystemExit('expected check block not found')
p.write_text(s.replace(block, '
'))
PY2
./gradlew clean check --dry-run > false-green-graph.txt
./gradlew clean check
cat false-green-graph.txt
find build/test-results -type f -name 'TEST-*.xml' -print | sort
The expected result is deliberately dangerous:
check can succeed while no fresh integration XML
exists. The integrationTest task itself still exists.
Prove that with ./gradlew help --task integrationTest.
Preserve false-green-graph.txt as the evidence of the
governance defect.
8. Prove the missing suite independently
./gradlew integrationTest
find build/test-results/integrationTest -type f -name 'TEST-*.xml' -print | sort
A passing direct integration run proves the suite is valid but disconnected. That distinction matters: do not “repair” the false green by rewriting the test itself.
9. Reconnect the gate, then inject one controlled failing integration test
mv build.gradle.kts.healthy build.gradle.kts
./gradlew check --dry-run > repaired-graph.txt
grep -F 'integrationTest' repaired-graph.txt || true
# Inject a controlled wrong expectation in the disposable test only.
cp src/integrationTest/java/dev/academy/testing/GreetingResourceIT.java GreetingResourceIT.java.ok
python - <<'PY2'
from pathlib import Path
p=Path('src/integrationTest/java/dev/academy/testing/GreetingResourceIT.java')
s=p.read_text()
p.write_text(s.replace('assertEquals("academy-integration",', 'assertEquals("WRONG-BANNER",'))
PY2
set +e
./gradlew clean check > controlled-failure.log 2>&1
status=$?
set -e
printf 'exit=%s
' "$status"
tail -n 35 controlled-failure.log
find build/test-results/integrationTest -type f -name 'TEST-*.xml' -print | sort
The build should now fail because the integration task is actually in the gate. Preserve its XML and the console excerpt. The unit suite may pass, but the verification lifecycle correctly propagates the integration failure.
10. Repair the test and re-prove the full gate
mv GreetingResourceIT.java.ok src/integrationTest/java/dev/academy/testing/GreetingResourceIT.java
./gradlew clean check
printf '%s
' '--- final task graph ---'
./gradlew check --dry-run
printf '%s
' '--- final XML ---'
find build/test-results -type f -name 'TEST-*.xml' -print | sort
printf '%s
' '--- report entry points ---'
ls -l build/reports/tests/test/index.html
ls -l build/reports/tests/integrationTest/index.html
Do not stop at BUILD SUCCESSFUL. Confirm both report
families were regenerated after the repair.
11. Verification checklist
- Wrapper reports Gradle 9.7.1 and the expected JVM.
- Java toolchain target is 17; JUnit is explicitly 6.1.3.
-
testRuntimeClasspathandintegrationTestRuntimeClasspathare separately inspectable. -
Healthy
check --dry-runincludes both test tasks. -
The false-green graph omits
integrationTestwhile the task still exists. - Direct integration execution produces fresh integration XML.
-
After reconnection, the deliberate failing assertion causes
checkto fail and records failure evidence. -
After repair, a clean
checkrecreates both unit and integration reports. -
No real secrets, external services, normal
~/.gradle, or production state were touched.
12. Cleanup and rollback
# Keep notes/logs elsewhere if desired, then delete only the disposable lab.
cd ..
rm -rf gradle-testing-checkpoint
In a real repository, rollback means reverting the lifecycle/test change in version control and rerunning the full verification gate. Cache deletion is not rollback.
13. Production operating model and bridge to Chapter 23
Chapter 22 adds an auditable test topology: source set → suite configuration → classpath → target Test task → filter → report → lifecycle gate. CI can now answer “which tests actually ran?” rather than merely “did Gradle return zero?”
Chapter 23 extends that precision to the JVM/toolchain boundary: the JDK that runs Gradle is not necessarily the JDK that compiles production code, runs annotation processors, compiles Kotlin/JVM, or executes cross-version tests. The same evidence discipline continues.
Knowledge check
What exact edit creates the false-green checkpoint state?
Removing only the check dependency on the
integration suite; the suite/task still exists but is not
scheduled by check.
How do you prove the false green is lifecycle-related rather than a broken suite?
Run integrationTest directly and inspect its fresh
report; it remains independently executable.
After reconnecting the suite, what should the deliberate wrong assertion do?
Make integrationTest and therefore
check fail, with failure details in integration
XML/HTML and console output.
Why compare unit and integration runtime classpaths?
They are distinct suite state; the integration suite needs an explicit project dependency for production output/resources.
What makes the final green result trustworthy?
A clean graph containing both suites plus fresh, distinct report evidence from both tasks—not the success line alone.
What does Chapter 23 add next?
Explicit Gradle-runtime JVM, Java toolchain, compiler target, annotation-processor, Kotlin/JVM, and cross-version execution boundaries.
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.