skip to content

Framework Wiring

Getting a test framework onto the classpath correctly: Jupiter, the Vintage engine, TestNG, and version alignment across them. Interviewers ask because engine and launcher mismatches produce baffling zero-test runs.

on this pageshow

explore

questions

20

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

How do you tell Gradle's Test task to run JUnit 5 (Jupiter) tests instead of the default JUnit 4 runner?

level: juniorimportance: must knowfreq 78%

basics

~10 s

Call useJUnitPlatform() on the test task. By default Gradle uses the JUnit 4 runner, so you must opt into the JUnit Platform for Jupiter tests to be detected and executed.

open as a page

A team adds junit-jupiter-api and calls useJUnitPlatform(), but the build reports zero tests run. What is the most likely cause and the fix?

level: juniorimportance: must knowfreq 55%

basics

~10 s

They added only the API, not the engine. Without junit-jupiter-engine on testRuntimeOnly the Platform has nothing to discover the tests. Add the engine (or the junit-jupiter aggregator) and the tests run.

open as a page

How do you configure a Gradle project to run tests with TestNG instead of the default JUnit?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Add the TestNG dependency to testImplementation, then call useTestNG() inside the test task. Without useTestNG(), Gradle assumes JUnit and won't detect @Test annotations from TestNG.

open as a page

What is the JUnit Vintage engine, and how do you enable it in a Gradle build so that legacy JUnit 4 tests run on the JUnit Platform?

level: juniorimportance: must knowfreq 60%

basics

~10 s

Vintage is a JUnit Platform TestEngine that runs JUnit 4 tests. Add testRuntimeOnly('org.junit.vintage:junit-vintage-engine') and call useJUnitPlatform() in the test task so Gradle discovers and runs them.

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

What is the difference between depending on org.junit.jupiter:junit-jupiter versus the individual junit-jupiter-api / junit-jupiter-engine artifacts, and where do they belong in Gradle configurations?

level: middleimportance: must knowfreq 62%

basics

~10 s

junit-jupiter is an aggregator that pulls in the API, params, and engine together. The API is what your tests compile against; the engine is runtime-only. Add the aggregator to testImplementation to cover both.

open as a page

How can you run JUnit 4 and JUnit 5 tests together in the same Gradle test task, and what makes that possible?

level: middleimportance: must knowfreq 55%

basics

~10 s

Call useJUnitPlatform() and put both engines on testRuntimeOnly: junit-jupiter-engine for JUnit 5 and junit-vintage-engine for JUnit 4. The Platform discovers both and runs each test with its matching engine.

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

Walk through how Gradle, after useJUnitPlatform(), discovers and runs your Jupiter tests at execution time.

level: middleimportance: should knowfreq 40%

basics

~20 s

Gradle starts a test worker, creates the Platform Launcher, which uses ServiceLoader to find the Jupiter engine, asks it to discover @Test classes on the test classpath, then executes them and streams results back to Gradle for reporting.

open as a page

How do you include or exclude TestNG groups from Gradle, and what does that control?

level: middleimportance: should knowfreq 45%

basics

~10 s

Inside useTestNG { ... } call includeGroups("smoke") and excludeGroups("slow"). TestNG groups are named tags on @Test methods, and these options select which tagged tests run.

open as a page

How do you point Gradle at a TestNG suite XML file, and when would you prefer suite-XML-driven discovery over Gradle's class detection?

level: middleimportance: should knowfreq 38%

basics

~10 s

Inside useTestNG { } call suites(file("src/test/resources/testng.xml")). Gradle then runs the tests listed in that XML, letting TestNG control ordering, grouping, and parallel config instead of Gradle's own class scan.

open as a page

After switching a Gradle module to useJUnitPlatform(), the build reports 'No tests found for given includes' or silently skips the old tests. What likely went wrong and how do you fix it?

level: middleimportance: should knowfreq 45%

basics

~10 s

The Vintage engine is missing from the test runtime classpath, so the Platform has no engine to run the JUnit 4 tests. Add testRuntimeOnly('org.junit.vintage:junit-vintage-engine') so they're discovered.

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

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%

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.

open as a page

What TestNGOptions can you set from Gradle to control parallel execution and behavior, and how do they relate to Gradle's own maxParallelForks?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Inside useTestNG { } you can set parallel = "methods", threadCount = N, plus listeners, preserveOrder, and useDefaultListeners. These run tests concurrently within one JVM, separate from Gradle's maxParallelForks which spawns multiple test JVMs.

open as a page

Which JUnit 4 features (e.g. @Rule, @RunWith runners, @Category) keep working under the Vintage engine, and which JUnit 5 features can JUnit 4 tests NOT use just by being run via Vintage?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Vintage faithfully runs JUnit 4 tests as JUnit 4: @Rule, @RunWith runners (Parameterized, Suite, SpringRunner), @Category, @Ignore all work. But Vintage doesn't grant them Jupiter features like @ParameterizedTest, extensions, or @Nested — those need a real rewrite.

open as a page

A legacy module uses TestNG and you're introducing JUnit 5 for new tests. How do you let both run under Gradle, and what are the constraints?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

A single Test task supports only one framework, so create a second source set with its own Test task: keep useTestNG() on the original, add useJUnitPlatform() on the new one. Wire the new task into check.

open as a page

You inherit a multi-module Gradle project with thousands of JUnit 4 tests. How would you introduce the Vintage engine to enable a safe, incremental migration to JUnit 5 across all modules, and how do you keep it from becoming permanent?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Add a convention plugin that wires useJUnitPlatform() plus both engines everywhere, keeping all JUnit 4 tests green. Write new tests in Jupiter, migrate old ones module by module, and track remaining Vintage usage so you can delete the engine when it hits zero.

open as a page