skip to content

Describe the three-part architecture of JUnit 5 (Platform, Jupiter, Vintage) and why it was split that way.

level: middleimportance: must knowfreq 55%

answer

  1. Platform = launcher + TestEngine SPI
  2. Jupiter = new API + engine
  3. Vintage = runs JUnit 3/4
  4. JUnit 4 fused API + runner + tooling
  5. engines plug into one Platform

basics

~20 s

JUnit 5 is three pieces: the Platform (launches tests and lets build tools/IDEs run them), Jupiter (the new way to write and run tests), and Vintage (an engine that runs your old JUnit 4 tests). JUnit 4 was a single library with no such separation.

solid answer

~50 s

JUnit 5 was deliberately split into three sub-projects. The **Platform** is the foundation: it defines a TestEngine SPI and the launcher that IDEs, Gradle, and Maven talk to, so they no longer need to know how each framework works internally. **Jupiter** is the new programming and extension model — the @Test, @BeforeEach, assertions, and @ExtendWith API — plus its own TestEngine. **Vintage** is a TestEngine that runs legacy JUnit 3/4 tests unchanged. The point of the split is decoupling: JUnit 4 fused the API, the runner, and the tool integration into one jar, so adding a feature (like Lambda support) or a new test style was hard. By separating the launcher from the engines, other frameworks (e.g. Spock, TestNG-style engines) can plug into the same Platform, and teams can run old and new tests side by side during migration.

go deeper

for a junior

Name the three parts and what each does in one line: Platform launches, Jupiter is the new API, Vintage runs old tests.

for a middle

Explain the TestEngine/Launcher split and that engines coexist on one Platform, enabling mixed old/new runs.

for a senior

Articulate the design motivation — JUnit 4 fused API/runner/tooling, the SPI decouples tools from frameworks and enables third-party engines.

for a principal

Discuss the SPI as an extensibility boundary, its impact on the testing ecosystem (Spock/jqwik/Cucumber as engines), and migration strategy choices it unlocks.

## The problem JUnit 4 had In JUnit 4 a *single* jar bundled three concerns: (1) the **API** you code against (`@Test`, assertions); (2) the **runner** that actually executes tests; and (3) the **tool integration** that IDEs and build tools used to discover and launch tests. Because they were fused, build tools hard-coded knowledge of JUnit 4's internals, and extending the model (custom runners via `@RunWith`, only *one* per class) was clumsy. You also couldn't easily mix test styles. ## The JUnit 5 answer: three sub-projects JUnit 5 = **Platform + Jupiter + Vintage**. ### 1. JUnit Platform — the foundation The Platform defines a small contract called the **TestEngine SPI** (Service Provider Interface — a Java interface that pluggable implementations fulfill). It also ships a **Launcher** API. The deal is: an IDE or build tool talks *only* to the Launcher; the Launcher discovers all `TestEngine` implementations on the classpath and asks each to find and run its tests. So tooling no longer needs to understand any specific framework — it understands *the Platform*. (`junit-platform-launcher`, `junit-platform-engine` artifacts.) ### 2. JUnit Jupiter — the new framework *Jupiter* is the name for everything new in JUnit 5. It has two halves: - **`junit-jupiter-api`** — the annotations and assertions you write tests with (`@Test`, `@BeforeEach`, `@ExtendWith`, `Assertions.*`). - **`junit-jupiter-engine`** — a `TestEngine` implementation that the Platform calls to actually run Jupiter tests. This is the part you normally depend on (`junit-jupiter` aggregates them). ### 3. JUnit Vintage — the backward-compatibility bridge *Vintage* is **another `TestEngine`** (`junit-vintage-engine`) that knows how to discover and run **JUnit 3 and JUnit 4** tests. Add it to the classpath and your existing `org.junit.Test` tests run *unchanged* on the same Platform, in the same build run, next to your new Jupiter tests. This is what makes migration *incremental* — you don't have to convert everything at once. ## Why this matters (the design payoff) - **Decoupling:** separating the launcher from the engines means build tools integrate once with the Platform, and *any* framework that ships a `TestEngine` (Spock, jqwik, Cucumber, etc.) runs through the same pipeline. - **Coexistence:** Jupiter and Vintage engines run together, enabling phased migration. - **Extensibility over inheritance:** within Jupiter, the old single-`@RunWith` runner is replaced by *composable* extensions (`@ExtendWith`), a model only possible because the engine/runner concern was cleanly carved out. ## A mental model Think of the **Platform** as a power strip, and **Jupiter** and **Vintage** as two appliances plugged into it; your IDE plugs into the strip, not into each appliance. Adding a third appliance (another engine) needs no change to the strip or the IDE.

  • If you only want to run new tests, which of the three pieces do you actually need on the classpath?
    The Platform (pulled in transitively) plus junit-jupiter (api + engine). You only add junit-vintage-engine when you still have JUnit 3/4 tests to run.
  • How does a non-JUnit framework like Spock run through JUnit 5?
    It ships its own TestEngine implementing the Platform's TestEngine SPI; the Launcher discovers it on the classpath and drives it exactly like the Jupiter or Vintage engine.

The Platform is a power strip; Jupiter and Vintage are appliances plugged in. Your IDE plugs into the strip, not each appliance — so any new appliance (engine) just works.

saying these in an interview costs you the question

  • Calling Jupiter and Vintage 'versions' rather than engines on the same Platform
  • Thinking Vintage rewrites/converts JUnit 4 tests — it runs them as-is
  • Believing JUnit 5 is one monolithic jar like JUnit 4
  • Confusing the Platform (launcher/SPI) with Jupiter (the test API)

context