skip to content

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?

level: juniorimportance: must knowfreq 58%

answer

  1. Platform = launcher + TestEngine SPI
  2. Jupiter = api + params + engine
  3. Vintage = runs JUnit 3/4 on the Platform
  4. Platform doesn't know what @Test is
  5. ServiceLoader finds engines

basics

~20 s

JUnit 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 s

JUnit 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 lines
java
import 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

for a junior

Name the three parts and one sentence each; make clear you write against Jupiter's API and that Vintage exists for old tests.

for a middle

Add the artifact names (api vs engine vs launcher) and explain that engines are found via ServiceLoader at runtime.

for a senior

Frame it as a plug-in architecture: the Platform decoupled tool vendors from JUnit internals, which is why Cucumber, Spock and ArchUnit ship engines.

for a principal

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

context