skip to content

The JUnit Platform can execute legacy JUnit 4 tests through an engine named junit-vintage-engine. Explain what that engine actually is, what kinds of test classes it accepts, and what it does NOT change about how those tests behave.

level: juniorimportance: must knowfreq 45%

answer

  1. Platform + Jupiter + Vintage
  2. TestEngine id junit-vintage
  3. adapter over JUnit 4 Runner + RunNotifier
  4. JUnit 4.12+ required
  5. no Jupiter extensions leak in

basics

~20 s

junit-vintage-engine is a TestEngine implementation for the JUnit Platform. It discovers JUnit 3 (junit.framework.TestCase) and JUnit 4 classes, delegates execution to JUnit 4's own runners, and reports results back to the platform. The tests keep pure JUnit 4 semantics and gain no Jupiter features.

solid answer

~50 s

"JUnit 5" is three pieces: the **Platform** (launcher plus the `TestEngine` SPI that IDEs and build tools talk to), **Jupiter** (the new programming model and its engine), and **Vintage** (`junit-vintage-engine`). Vintage is simply another `TestEngine`: at discovery it builds a JUnit 4 `Runner` for each legacy class and converts JUnit 4's `Description` tree into platform `TestDescriptor`s under the engine id `junit-vintage`; at execution it runs that runner with a `RunNotifier` whose callbacks it translates into platform events. It accepts JUnit 3 `TestCase` subclasses and JUnit 4 classes, including `@RunWith`-driven ones, and requires JUnit 4.12 or newer at test runtime. What it does *not* do is change semantics. Legacy tests still obey `@Before`/`@After`/`@BeforeClass`, `@Rule`/`@ClassRule`, `expected=` and `timeout=` on `@Test`, `@Ignore`, and their custom runners. They get no Jupiter features: no `@ExtendWith`, no `ParameterResolver`, no `@Nested`. Vintage buys one unified test run and uniform reporting, not modernization.

code

java · 23 lines
java
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.assertTrue;

public class LegacyFileTest {

    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    private FileStore store;

    @Before
    public void setUp() {
        store = new FileStore(folder.getRoot());
    }

    @Test(timeout = 500)
    public void writesFile() throws Exception {
        assertTrue(store.write("a.txt", "hi"));
    }
}

go deeper

for a junior

Recall the three-part structure (Platform, Jupiter, Vintage) and say plainly that Vintage runs old JUnit 3/4 tests unchanged on the new platform.

for a middle

Explain the TestEngine SPI role: discovery builds a JUnit 4 Runner, execution adapts RunNotifier events, unique ids carry the junit-vintage prefix, and JUnit 4.12+ must be present.

for a senior

Frame it as a compatibility bridge with a cost — two programming models, no shared extension mechanism — and mention engine-level filtering to measure remaining legacy tests.

for a principal

Position Vintage in a migration strategy: it decouples the build/reporting upgrade from the test-code rewrite, and the organizational risk is that the bridge becomes permanent without a burn-down target.

## The three pieces of "JUnit 5" JUnit 5 is not a single artifact. It is the **JUnit Platform** (a launcher plus the `TestEngine` service-provider interface that IDEs, Gradle and Maven speak to), **JUnit Jupiter** (the new annotations such as `org.junit.jupiter.api.Test` plus the engine that runs them), and **JUnit Vintage** (`junit-vintage-engine`, an engine that runs the old stuff). The platform itself knows nothing about annotations; it only knows how to ask each registered engine to *discover* tests and then *execute* them. ## What Vintage is Vintage is a `TestEngine` whose id is `junit-vintage`. It is a thin adapter over JUnit 4's public runner API: - **Discovery.** For every candidate class it asks JUnit 4's `RunnerBuilder` for a `Runner`. That is exactly the same machinery `JUnitCore` used in JUnit 4, so whatever runner would have been chosen then is chosen now: the default `BlockJUnit4ClassRunner`, or whatever `@RunWith` names (`Parameterized`, `Suite`, `MockitoJUnitRunner`, `SpringRunner`, `Enclosed`, ...). The runner's `Description` tree is converted into platform `TestDescriptor`s, giving unique ids like `[engine:junit-vintage]/[runner:com.acme.LegacyTest]/[test:shouldWork(com.acme.LegacyTest)]`. - **Execution.** Vintage hands the runner a `RunNotifier` and maps JUnit 4 callbacks onto platform outcomes: `testStarted`/`testFinished` become started/successful, `testFailure` becomes failed, `testAssumptionFailure` becomes *aborted*, `testIgnored` becomes *skipped*. ## What it accepts Any class JUnit 4 itself could run: JUnit 4 classes with `org.junit.Test` methods, `@RunWith`-annotated classes, JUnit 4 `Suite`s, and JUnit 3.8 style classes extending `junit.framework.TestCase` (JUnit 4's runner infrastructure has always handled those via `JUnit38ClassRunner`). Vintage needs a real JUnit 4 jar present at test runtime — 4.12 is the documented minimum, and 4.13.2 is the practical choice. ## What it deliberately does not change This is the point interviewers probe. Running under the platform does **not** upgrade the tests: - Lifecycle stays JUnit 4: `@Before`, `@After`, `@BeforeClass`, `@AfterClass`, a fresh instance per test method as the runner decides. - `@Rule`, `@ClassRule` and custom runners keep working — that is the whole reason the engine exists. - `expected = SomeException.class`, `timeout = 500`, `@Ignore` keep their JUnit 4 meaning. - Jupiter's extension model is invisible to these tests: `@ExtendWith`, `ParameterResolver`, `TestInstancePostProcessor`, `@Nested`, `@ParameterizedTest`, `@DisplayName`, `Assertions.assertAll` — none of it applies. Conversely, JUnit 4 rules and runners have no effect on Jupiter tests. - Jupiter-only configuration such as its parallel-execution settings does not reach Vintage. A useful mental model: the platform gives you one *run* and one *report*; each engine still owns its own *programming model*. ## Why you would keep it Two reasons dominate. First, size: a codebase with thousands of JUnit 4 tests cannot be rewritten in one change, and Vintage lets new tests be written in Jupiter today while the old ones keep passing. Second, third-party runners: a framework whose only integration is a `@RunWith` runner still needs JUnit 4 to run. The standing cost is two programming models in one repository — two ways to express setup, two ways to extend, two idioms in review — which is why Vintage is normally treated as a bridge with an end date rather than a permanent state. ## Selecting engines Because each engine has an id, a run can be restricted to one of them: the platform launcher accepts engine filters (`includeEngines("junit-vintage")` / `excludeEngines`, `--select-engine` on the ConsoleLauncher, `@SelectEngines` on a platform suite). That is handy for asking "how many legacy tests do we still have?" without touching source code.

  • Can a JUnit 4 test running under the vintage engine use a Jupiter extension such as a @ExtendWith-registered ParameterResolver?
    No. Extensions are a Jupiter-engine concept; the vintage engine hands the class to a JUnit 4 runner that has never heard of them, so the annotation is simply ignored. The JUnit 4 equivalents are rules and custom runners. If you need the extension, the class has to be migrated to Jupiter.
  • How does the vintage engine report a JUnit 4 assumption failure, for example Assume.assumeTrue(false)?
    JUnit 4 signals it through RunNotifier.fireTestAssumptionFailure, and Vintage maps that to the platform's *aborted* outcome rather than failed or skipped. Reports therefore distinguish it from an @Ignore'd test, which arrives via testIgnored and shows up as skipped.
  • Does the vintage engine handle JUnit 3 tests too?
    Yes. Classes extending junit.framework.TestCase are picked up because JUnit 4's runner builder falls back to JUnit38ClassRunner for them, and Vintage just drives that runner. So a single platform run can span JUnit 3, JUnit 4 and Jupiter tests.

Vintage is a translator at a conference, not a teacher: the old speaker keeps speaking the old language exactly as before, and only the transcript everyone reads comes out in the new format.

saying these in an interview costs you the question

  • Saying the vintage engine "converts" or "rewrites" JUnit 4 tests into JUnit 5 tests
  • Believing Jupiter annotations such as @ExtendWith or @Nested start working once tests run on the platform
  • Thinking Vintage replaces the JUnit 4 library, so the junit:junit dependency can be dropped
  • Claiming JUnit 3 TestCase classes cannot run on the JUnit Platform at all
  • Assuming the vintage engine is required to run Jupiter tests

context