JUnit 5 is usually described as three sub-projects rather than one library. What are they, and what is each one responsible for when tests actually run?
answer
- Platform = launcher + TestEngine SPI
- Jupiter = api + params + engine
- Vintage = runs JUnit 3/4 on the Platform
- Platform doesn't know what @Test is
- ServiceLoader finds engines
basics
~20 sJUnit 5 = Platform + Jupiter + Vintage. The Platform is the foundation: it launches tests and defines the TestEngine plug-in interface. Jupiter is the new programming model (annotations, assertions) plus its own engine. Vintage is an engine that runs old JUnit 3 and 4 tests.
solid answer
~50 sJUnit 5 is an umbrella over three sub-projects. - **JUnit Platform** — the foundation everything else plugs into. It defines the `TestEngine` SPI and ships the `Launcher` that IDEs and build tools call. The Platform itself knows nothing about `@Test`; it only knows how to ask engines to discover and execute tests and how to report the results. - **JUnit Jupiter** — the new programming model and extension model. `junit-jupiter-api` gives you `@Test`, `@BeforeEach`, `Assertions`, `@ParameterizedTest`, `Extension`; `junit-jupiter-engine` is the `TestEngine` implementation that finds and runs those tests on the Platform. - **JUnit Vintage** — `junit-vintage-engine`, a `TestEngine` that runs existing JUnit 3.8 and JUnit 4 tests on the same Platform, so old and new tests execute in one run. The practical payoff: the Platform is a shared launching surface, so non-JUnit frameworks (Cucumber, ArchUnit, Spock, jqwik) can ship their own engines and be run by the same IDE and build-tool machinery.
code
java · 13 linesimport org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CartTest {
@Test
void totalsTwoItems() {
Cart cart = new Cart();
cart.add(new Item("pen", 2));
cart.add(new Item("pad", 3));
assertEquals(5, cart.total());
}
}go deeper
Name the three parts and one sentence each; make clear you write against Jupiter's API and that Vintage exists for old tests.
Add the artifact names (api vs engine vs launcher) and explain that engines are found via ServiceLoader at runtime.
Frame it as a plug-in architecture: the Platform decoupled tool vendors from JUnit internals, which is why Cucumber, Spock and ArchUnit ship engines.
Discuss the consequence for an organisation's tooling strategy — one launching surface means reporting, tagging and CI integration are written once against the Platform rather than per framework.
## Why JUnit 5 is not one library JUnit 4 was a single jar that did everything: it defined `@Test`, it discovered tests by reflection, it ran them, and IDEs integrated with it by reaching into its internals (`JUnitCore`, `Runner`, `RunNotifier`). That coupling was the problem JUnit 5 set out to fix. Tool vendors were pinned to JUnit 4 internals, and every alternative testing framework had to pretend to be a JUnit 4 `Runner` to get a green tick in an IDE. JUnit 5 therefore ships as three sub-projects that are released together but have distinct jobs. ## 1. JUnit Platform — the foundation The Platform is the layer that tools talk to. Its key artifacts: - `junit-platform-commons` — shared internal utilities. - `junit-platform-engine` — the **SPI** (service provider interface) that any test framework implements: `TestEngine`, with `discover(...)` and `execute(...)`, plus `TestDescriptor` and `UniqueId`. - `junit-platform-launcher` — the **client-facing API**. An IDE or build tool builds a discovery request, calls the `Launcher`, receives a `TestPlan`, and listens for execution events. - `junit-platform-console` — a standalone launcher you can run from a plain command line. - `junit-platform-suite` — declarative suites (`@Suite`) that run across engines. Crucially, the Platform contains **no test annotations**. It does not know what `@Test` means. It knows only "find engines on the classpath via `ServiceLoader`, ask each one what tests it has, then ask it to run them and tell me what happened". ## 2. JUnit Jupiter — the new programming model Jupiter is what you actually write against: - `junit-jupiter-api` — `@Test`, `@BeforeEach`/`@AfterEach`, `@BeforeAll`/`@AfterAll`, `@Nested`, `@DisplayName`, `@Disabled`, `Assertions.assertEquals/assertThrows/assertAll`, `Assumptions`, and the `Extension` interfaces (`@ExtendWith`). - `junit-jupiter-params` — `@ParameterizedTest` and its argument sources. - `junit-jupiter-engine` — the `TestEngine` implementation, registered under the engine id `junit-jupiter`, that scans classes for Jupiter annotations, builds the descriptor tree, and executes it with lifecycle callbacks and extensions. So "Jupiter" = the API you compile against **plus** the engine that understands it. They are deliberately different artifacts (see the api-vs-engine question). ## 3. JUnit Vintage — backward compatibility `junit-vintage-engine` is a `TestEngine` with the id `junit-vintage` that delegates to the real JUnit 4 runner infrastructure. Put it on the test runtime classpath together with `junit:junit` 4.12+ and your existing JUnit 4 classes — including `@RunWith`, `@Rule`, `@Category`, and JUnit 3 `TestCase` subclasses — keep running, side by side with new Jupiter tests, in the same run and the same report. Vintage exists so that migration is incremental: you never need a big-bang rewrite of a legacy suite before you can write your first Jupiter test. ## How a run flows end to end 1. Your IDE or build tool builds a `LauncherDiscoveryRequest` ("everything under `com.example`"). 2. The `Launcher` finds all `TestEngine` implementations on the classpath through `ServiceLoader`. 3. Each engine performs discovery and returns a tree of `TestDescriptor`s rooted at its own unique id, e.g. `[engine:junit-jupiter]/[class:com.example.CartTest]/[method:total()]`. 4. The Launcher merges those trees into one `TestPlan` and hands it to registered listeners. 5. Execution runs engine by engine; listeners receive started/finished/skipped events, and the tool renders the familiar green/red tree. ## Versioning In the JUnit 5 line the version numbers deliberately differ: Platform artifacts were `1.x` while Jupiter and Vintage were `5.x` — they are separate products with separate compatibility promises. JUnit 6.0 (2025) unified the numbering so Platform, Jupiter and Vintage all carry the same `6.x` version, and raised the baseline to Java 17. If you see `junit-platform-launcher:1.11.0` next to `junit-jupiter:5.11.0`, that mismatch is normal for the 5.x line, not a bug. ## Why the split matters in practice Because the Platform is framework-agnostic, non-JUnit tools ship engines and get first-class IDE and build support for free: Cucumber (`cucumber-junit-platform-engine`), ArchUnit, Spock 2.x, jqwik, Kotest. That is the real architectural win — the Platform turned "running tests in Java" into a plug-in ecosystem rather than a JUnit 4 monopoly.
- Which of the three sub-projects does an IDE integrate against?The Platform — specifically `junit-platform-launcher`. The IDE builds a discovery request, calls the `Launcher`, and consumes the resulting `TestPlan` and execution events. It never talks to the Jupiter engine directly, which is exactly why the same IDE view can display Cucumber or ArchUnit results.
- If you only ever write Jupiter tests, do you still need the Platform?Yes, always. Jupiter's engine implements a Platform SPI and can only be driven by a Platform `Launcher`; there is no way to run a Jupiter test without it. You usually don't declare it explicitly because the engine depends on `junit-platform-engine` transitively and your IDE or build tool supplies the launcher.
- Can the Jupiter engine run JUnit 4 tests?No. The Jupiter engine only understands Jupiter annotations from `junit-jupiter-api`. A class using `org.junit.Test` from JUnit 4 is invisible to it, which is why the separate vintage engine exists. Silently skipped legacy tests are the classic symptom of expecting otherwise.
The Platform is a power socket standard, Jupiter and Vintage are two appliances plugged into it, and Cucumber or ArchUnit are third-party appliances that fit the same socket.
saying these in an interview costs you the question
- Saying JUnit 5 is just "JUnit 4 with new annotations" in one jar
- Thinking the Platform contains @Test and the assertions
- Believing the Jupiter engine also executes JUnit 4 tests
- Assuming Platform 1.x next to Jupiter 5.x is a broken dependency set
- Claiming Vintage is required for new projects