Chapter 22Lesson 05~265 minutes

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.

CheckpointUnit testsIntegration testsLifecycle proofRollback

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 check by 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.
  • check schedules 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

Healthy checkpoint graph
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.
  • testRuntimeClasspath and integrationTestRuntimeClasspath are separately inspectable.
  • Healthy check --dry-run includes both test tasks.
  • The false-green graph omits integrationTest while the task still exists.
  • Direct integration execution produces fresh integration XML.
  • After reconnection, the deliberate failing assertion causes check to fail and records failure evidence.
  • After repair, a clean check recreates 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?

How do you prove the false green is lifecycle-related rather than a broken suite?

After reconnecting the suite, what should the deliberate wrong assertion do?

Why compare unit and integration runtime classpaths?

What makes the final green result trustworthy?

What does Chapter 23 add next?

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.