skip to content

Why might a Gradle build need to declare org.junit.platform:junit-platform-launcher explicitly on testRuntimeOnly, and what role does the launcher play?

level: seniorimportance: should knowfreq 48%

answer

  1. launcher = Launcher API impl
  2. Gradle worker is a launcher client
  3. no longer added implicitly
  4. testRuntimeOnly placement
  5. align with Platform/BOM version

basics

~20 s

The 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 s

The **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 lines
kotlin
dependencies {
    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

for a junior

Just know the line testRuntimeOnly(...junit-platform-launcher) may be required.

for a middle

Explain that the launcher is what Gradle uses to discover/run tests and goes on testRuntimeOnly.

for a senior

Discuss the implicit-launcher removal, version alignment with the engine, and ServiceLoader discovery.

for a principal

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.

context