skip to content

Version Alignment via BOM

Aligning JUnit platform, Jupiter, and launcher versions by importing the junit-bom platform. Interviewers ask because a mismatched launcher is the classic cause of 'no tests found'.

on this pageshow

questions

5

What is the JUnit BOM and why would you use `testImplementation(platform("org.junit:junit-bom:..."))` in a Gradle build instead of pinning each JUnit artifact's version individually?

level: juniorimportance: must knowfreq 60%

answer

  1. JUnit = Platform + Jupiter + Vintage, different version lines
  2. BOM = pom with dependencyManagement only
  3. platform(...) imports constraints, deps omit version
  4. avoids engine/launcher mismatch
  5. single line to bump

basics

~20 s

The JUnit BOM is a Bill of Materials that declares one consistent version for all JUnit modules (Jupiter, Platform, Vintage). Importing it with platform(...) lets you list JUnit dependencies without versions, so they all align.

solid answer

~40 s

JUnit 5 ships as several separately-versioned modules: `junit-jupiter-api`, `junit-jupiter-engine`, `junit-platform-launcher`, `junit-vintage-engine`, etc. The Platform, Jupiter, and Vintage components actually use *different* version numbers (e.g. Platform 1.10.x vs Jupiter 5.10.x). Manually pinning each is error-prone and easily produces a mismatch where, say, the Jupiter engine is newer than the launcher Gradle runs. The `junit-bom` is a Maven BOM (`pom` with `dependencyManagement`) that maps every JUnit module to a mutually-compatible version. In Gradle you import it with `testImplementation(platform("org.junit:junit-bom:5.10.2"))`; then you declare the actual JUnit artifacts with **no version**, and the BOM-managed constraint supplies it. This keeps API, engine, and launcher in lockstep and gives you a single line to bump.

code

kotlin · 5 lines
kotlin
dependencies {
    testImplementation(platform("org.junit:junit-bom:5.10.2"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

go deeper

for a junior

Know that the BOM gives one consistent version and lets you omit per-artifact versions.

for a middle

Explain the three sub-projects and the distinct version lines, and that platform() imports dependencyManagement constraints.

for a senior

Discuss how the BOM constrains transitives, platform vs enforcedPlatform, and the mismatch symptoms it prevents.

for a principal

Frame BOM usage as a dependency-governance policy across many modules and how to standardize it via a version catalog/convention plugin.

## The problem the BOM solves JUnit 5 is not one jar. It is an umbrella for three sub-projects, each independently versioned: - **JUnit Platform** — the test-engine SPI plus the launcher (`junit-platform-launcher`, `junit-platform-engine`). Versions look like `1.10.x`. - **JUnit Jupiter** — the modern programming/extension model: `junit-jupiter-api`, `junit-jupiter-engine`, `junit-jupiter-params`. Versions look like `5.10.x`. - **JUnit Vintage** — `junit-vintage-engine`, runs legacy JUnit 3/4 tests on the Platform. Also `5.10.x`. Because Platform (`1.x`) and Jupiter/Vintage (`5.x`) use *different* numbering, hand-pinning each artifact invites a silent mismatch — e.g. a Jupiter engine that depends on a newer Platform API than the launcher on the classpath provides. The symptom is usually `NoSuchMethodError`/`NoClassDefFoundError` from the launcher at test time, or "no tests found". ## What a BOM is A **BOM (Bill of Materials)** is a POM file of packaging `pom` whose only payload is a `dependencyManagement` block: a list of `group:artifact -> version` constraints. It declares *no* dependencies itself; it only says "if this module is on the graph, use this version". The `org.junit:junit-bom` BOM maps every JUnit module to a set of versions that the JUnit team tested together. ## Importing a BOM in Gradle Gradle has first-class BOM support via the `platform()` (and `enforcedPlatform()`) dependency notation: ```kotlin dependencies { testImplementation(platform("org.junit:junit-bom:5.10.2")) testImplementation("org.junit.jupiter:junit-jupiter") // no version testRuntimeOnly("org.junit.platform:junit-platform-launcher") // no version } ``` `platform(...)` imports the BOM's constraints into the configuration. The subsequent dependency declarations omit the version; Gradle's resolution applies the BOM-managed version. (`junit-jupiter` is itself an aggregator that pulls api + engine + params.) ## Why `platform` and not just a version string Applying the BOM constrains the *whole* dependency graph, not just the lines you wrote. If a transitive dependency drags in an older `junit-platform-commons`, the BOM constraint still aligns it. With `platform()` the constraint participates in normal conflict resolution (it can be overridden by a stronger declaration); `enforcedPlatform()` would force it and override transitives — generally avoid that for JUnit. ## One line to bump Upgrading JUnit becomes a single version change on the BOM coordinate (ideally in a version catalog), instead of editing five artifact lines that must move in lockstep.

  • Why do JUnit Platform and Jupiter have different version numbers (1.x vs 5.x)?
    They are separate sub-projects released together but versioned independently: Platform is the engine SPI/launcher infrastructure (1.x line started when JUnit 5 = '5'), Jupiter is the new programming model (5.x). The BOM exists precisely to map these distinct lines to a compatible set.
  • If you forget the BOM and just write `testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")`, does it still work?
    Often yes for that single artifact, because junit-jupiter declares its own compatible transitives. The BOM matters most when you mix Jupiter + Vintage + explicit Platform artifacts, or when a transitive drags in an off-version Platform module — then the BOM keeps everything aligned.

A BOM is like a packing list for a furniture kit: it doesn't ship any parts itself, it just guarantees every part you pull is from the same compatible set.

saying these in an interview costs you the question

  • Claiming the BOM 'downloads' or 'contains' the JUnit jars — it only declares version constraints.
  • Saying all JUnit modules share one version number — Platform (1.x) differs from Jupiter/Vintage (5.x).

context

open as a page

A build compiles fine but at test time fails with errors from `junit-platform-launcher` (e.g. NoSuchMethodError / 'no tests found'). How does a JUnit version mismatch cause this, and how does the BOM prevent it?

level: middleimportance: must knowfreq 55%

basics

~20 s

The Jupiter engine and the platform launcher on the test classpath are different, incompatible versions, so the launcher can't load/run the engine. The BOM pins engine, api, and launcher to one compatible set, removing the mismatch.

open as a page

A teammate imports the JUnit BOM and declares versionless JUnit dependencies, but tests still won't run. Walk through what the BOM does and does NOT do, and the other pieces that must be present for JUnit Platform tests to execute under Gradle.

level: middleimportance: should knowfreq 40%

basics

~20 s

The BOM only aligns versions; it doesn't make tests run. You still need useJUnitPlatform() on the test task, the Jupiter engine on the runtime classpath, and a launcher. The BOM just guarantees those are mutually compatible.

open as a page

How do you wire the JUnit BOM through a `libs.versions.toml` version catalog so a single version key aligns all JUnit modules across a multi-module Gradle build?

level: middleimportance: should knowfreq 45%

basics

~10 s

Declare one junit version and a junit-bom library in libs.versions.toml, then in each module do testImplementation(platform(libs.junit.bom)) plus versionless JUnit libraries. One key change updates every module.

open as a page

When aligning JUnit via a BOM, what is the difference between `platform(...)` and `enforcedPlatform(...)`, and which should you prefer for `junit-bom`? What happens when another BOM (e.g. Spring Boot's) also manages JUnit?

level: seniorimportance: should knowfreq 35%

basics

~10 s

platform() imports BOM versions as constraints that participate in normal conflict resolution (overridable). enforcedPlatform() forces them, overriding any other declaration. Prefer platform(junit-bom) so it cooperates with other BOMs instead of fighting them.

open as a page