skip to content

What is the JUnit Vintage engine, and how do you enable it in a Gradle build so that legacy JUnit 4 tests run on the JUnit Platform?

level: juniorimportance: must knowfreq 60%

answer

  1. Platform = launcher, engines = pluggable
  2. Vintage runs JUnit 4 on Platform
  3. useJUnitPlatform() on Test task
  4. testRuntimeOnly for the engine
  5. junit:junit still needed to compile

basics

~10 s

Vintage is a JUnit Platform TestEngine that runs JUnit 4 tests. Add testRuntimeOnly('org.junit.vintage:junit-vintage-engine') and call useJUnitPlatform() in the test task so Gradle discovers and runs them.

solid answer

~40 s

The JUnit Platform is the launcher/discovery layer; it runs tests through pluggable `TestEngine`s. Jupiter is the engine for JUnit 5 tests; **Vintage** is a backward-compat engine that runs old JUnit 4 (and JUnit 3) tests on that same platform. In Gradle you switch the `Test` task to the platform with `tasks.named('test') { useJUnitPlatform() }`, then put the Vintage engine on the test runtime classpath: `testRuntimeOnly('org.junit.vintage:junit-vintage-engine')`. You also still need the JUnit 4 API (`junit:junit:4.13.2`) on `testImplementation` so the `@Test`/`@RunWith` annotations compile. With both engines present, Vintage discovers and executes the JUnit 4 classes while Jupiter handles any JUnit 5 classes — letting a codebase migrate incrementally instead of all at once.

code

kotlin · 8 lines
kotlin
dependencies {
    testImplementation("junit:junit:4.13.2")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2")
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}

go deeper

for a junior

Know the two lines: useJUnitPlatform() plus the testRuntimeOnly vintage-engine dependency, and that Vintage runs old JUnit 4 tests.

for a middle

Explain the Platform/engine architecture and why the engine is testRuntimeOnly while the JUnit 4 API stays on testImplementation.

for a senior

Discuss incremental migration strategy, BOM/version alignment, and starter-test's junit:junit exclusion.

for a principal

Frame Vintage as a transitional bridge with a sunset plan; set policy to keep new code on Jupiter and burn down Vintage usage over time.

## The JUnit 5 architecture JUnit 5 is not a monolith — it is three sub-projects: - **JUnit Platform** — the foundation. It defines the `TestEngine` SPI and provides the `Launcher` that build tools (Gradle, Maven, IDEs) call to *discover* and *execute* tests. Gradle talks only to the Platform. - **JUnit Jupiter** — the new programming model (`org.junit.jupiter.api.Test`, `@BeforeEach`, extensions) **and** the `JupiterTestEngine` that runs those tests. - **JUnit Vintage** — a `TestEngine` (`VintageTestEngine`) that knows how to run **JUnit 3 and JUnit 4** tests on the Platform, for backward compatibility. The key idea: the Platform runs *any* registered engine. So a single test task can run JUnit 4 and JUnit 5 tests side by side, each handled by its own engine. ## Wiring it in Gradle Two things must be true: 1. **The `Test` task must use the JUnit Platform**, not the old JUnit 4 runner. That is `useJUnitPlatform()`. Without it, Gradle uses its legacy JUnit-4-only execution path and never loads any TestEngine. 2. **The Vintage engine must be on the test *runtime* classpath** so the Platform can discover it via the `ServiceLoader`. Engines are discovered at runtime, so the correct configuration is `testRuntimeOnly`, not `testImplementation` — your production/test source never references engine classes directly. You also keep the **JUnit 4 API** dependency (`junit:junit`) on `testImplementation` because your test *source code* imports `org.junit.Test`, `@RunWith`, `@Rule`, etc., and must compile against them. ```kotlin dependencies { testImplementation("junit:junit:4.13.2") // JUnit 4 API to compile against testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2") // engine to run them } tasks.named<Test>("test") { useJUnitPlatform() } ``` ## Why testRuntimeOnly and not testImplementation `testImplementation` puts a dependency on both the compile and runtime classpaths of the test source set. `testRuntimeOnly` puts it on the runtime classpath only. The engine is loaded by the Platform's `ServiceLoader` at execution time; no test code imports `VintageTestEngine`, so exposing it at compile time would only pollute auto-complete and the compile classpath. Using `testRuntimeOnly` is the conventional, minimal-surface choice. ## Spring Boot convenience With the Spring Boot dependency-management/BOM, `org.springframework.boot:spring-boot-starter-test` already pulls JUnit Jupiter and version-aligns the platform. To add Vintage you typically declare `testRuntimeOnly("org.junit.vintage:junit-vintage-engine")` without a version (the BOM supplies it) plus the JUnit 4 API. Starter-test historically *excluded* the old `junit:junit`, so you re-add it explicitly when you still have JUnit 4 tests.

  • Why is the Vintage engine declared with testRuntimeOnly rather than testImplementation?
    The Platform discovers engines via ServiceLoader at execution time; no test source imports engine classes, so it only needs to be on the runtime classpath. testRuntimeOnly keeps it off the compile classpath.
  • If you add Vintage but forget useJUnitPlatform(), what happens?
    Gradle falls back to its legacy JUnit 4 execution path which never loads any TestEngine. The Vintage engine is ignored, and JUnit 5 tests won't run at all.

saying these in an interview costs you the question

  • Saying Vintage runs JUnit 5 tests — Jupiter does that; Vintage runs JUnit 3/4.
  • Putting the engine on testImplementation/implementation instead of testRuntimeOnly.
  • Forgetting that the JUnit 4 API (junit:junit) is still required to compile the tests.

context