Describe the three-part architecture of JUnit 5 (Platform, Jupiter, Vintage) and why it was split that way.
answer
- Platform = launcher + TestEngine SPI
- Jupiter = new API + engine
- Vintage = runs JUnit 3/4
- JUnit 4 fused API + runner + tooling
- engines plug into one Platform
basics
~20 sJUnit 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 sJUnit 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
Name the three parts and what each does in one line: Platform launches, Jupiter is the new API, Vintage runs old tests.
Explain the TestEngine/Launcher split and that engines coexist on one Platform, enabling mixed old/new runs.
Articulate the design motivation — JUnit 4 fused API/runner/tooling, the SPI decouples tools from frameworks and enables third-party engines.
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)