Why might a Gradle build need to declare org.junit.platform:junit-platform-launcher explicitly on testRuntimeOnly, and what role does the launcher play?
answer
- launcher = Launcher API impl
- Gradle worker is a launcher client
- no longer added implicitly
- testRuntimeOnly placement
- align with Platform/BOM version
basics
~20 sThe launcher is the JUnit Platform API Gradle uses to discover and run tests. Newer Gradle versions no longer bundle it, so you add junit-platform-launcher on testRuntimeOnly to keep its version consistent and avoid missing-launcher errors.
solid answer
~40 sThe **JUnit Platform Launcher** (`org.junit.platform:junit-platform-launcher`) is the entry point Gradle's test worker uses to drive the Platform: it builds a `LauncherDiscoveryRequest`, discovers tests across all registered `TestEngine`s, and executes them while emitting events. Historically Gradle injected a launcher implicitly. From Gradle 8.x (and especially 9), Gradle stopped silently adding it to the test runtime classpath, so projects must declare `testRuntimeOnly("org.junit.platform:junit-platform-launcher")` themselves. Declaring it explicitly also lets you align its version with your Jupiter version (they share the JUnit 5 release train) and avoids a stale launcher being dragged in transitively. Symptoms of a missing or mismatched launcher are 'Could not find org.junit.platform.launcher...' or zero tests discovered. Put it on `testRuntimeOnly` because no test code imports launcher classes.
code
kotlin · 7 linesdependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
// Gradle no longer adds this automatically:
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.2")
}
tasks.test { useJUnitPlatform() }go deeper
Just know the line testRuntimeOnly(...junit-platform-launcher) may be required.
Explain that the launcher is what Gradle uses to discover/run tests and goes on testRuntimeOnly.
Discuss the implicit-launcher removal, version alignment with the engine, and ServiceLoader discovery.
Mandate launcher + BOM in a convention plugin so no module breaks on a Gradle upgrade; treat the implicit-launcher removal as a fleet-wide migration.
## What the Launcher is The JUnit 5 architecture has three layers: 1. **Platform** — the foundation: defines the `TestEngine` SPI and the **Launcher API**. 2. **Engines** — `junit-jupiter-engine` (JUnit 5 tests), `junit-vintage-engine` (JUnit 4), or third-party engines. 3. **Launcher** — `junit-platform-launcher`, the *implementation* of the Launcher API that orchestrates discovery and execution. A build tool or IDE is a **Launcher client**. Gradle's test worker calls `LauncherFactory.create()`, builds a discovery request, asks every engine on the classpath what tests it finds, then executes them and forwards events back to Gradle for reporting. ## Why declare it explicitly now For years Gradle added a launcher to the test runtime classpath automatically, so most build files never mentioned it. That implicit behavior was **removed** — Gradle now expects you to bring your own launcher. If it is absent, the test worker cannot start the Platform and you see errors or simply 'no tests found'. ```kotlin dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } ``` ## Version alignment The Platform and Jupiter use **different but coupled version numbers** (Platform 1.10.x ↔ Jupiter 5.10.x). Letting the launcher come in transitively risks a version that drifts from your engine. Declaring it (ideally through a version catalog or the JUnit BOM) keeps Platform and engine in lockstep. Mismatches can throw `LinkageError`s or discovery failures. ## Runtime-only placement No production or test source imports launcher types — only Gradle's worker does. Hence `testRuntimeOnly`, mirroring the engine.
- How does the launcher find the Jupiter engine at runtime?Via Java's ServiceLoader / ServiceLoaderTestEngineRegistry: the engine jar ships a META-INF/services/org.junit.platform.engine.TestEngine file, so the launcher discovers all engines on the classpath automatically.
- What error signals a missing launcher?Typically a 'Could not resolve / could not find org.junit.platform.launcher.LauncherFactory' or the test task reporting that no tests were found despite Jupiter tests existing.
- Why do Platform and Jupiter have different version numbers?They are released together but versioned independently: Platform tracks 1.x, Jupiter tracks 5.x, Vintage tracks 5.x. The JUnit BOM maps a single release to the correct trio.
saying these in an interview costs you the question
- Assuming Gradle always bundles the launcher — newer versions do not.
- Hardcoding a Platform version that drifts from the Jupiter engine version.