skip to content

Compare activating TestNG (useTestNG()) versus running legacy JUnit 4 tests under the JUnit Platform. What classpath and selector choices are involved?

level: seniorimportance: should knowfreq 35%

answer

  1. useTestNG() = TestNGOptions, suiteXmlFiles, groups
  2. TestNG not on the Platform
  3. Vintage engine = JUnit 4 on Platform
  4. junit-vintage-engine testRuntimeOnly
  5. keep useJUnitPlatform(), don't revert

basics

~10 s

useTestNG() switches the task to TestNG and exposes TestNG options. To run old JUnit 4 tests on JUnit 5, keep useJUnitPlatform() and add the junit-vintage-engine, which discovers JUnit 4 tests via the Platform.

solid answer

~50 s

These are two different migration stories. **TestNG:** `useTestNG()` selects the TestNG framework on the `Test` task and exposes `TestNGOptions` (e.g. `suiteXmlFiles`, `includeGroups`, `excludeGroups`, parallel mode). You also add the `org.testng:testng` dependency. TestNG is a self-contained framework — it is *not* run through the JUnit Platform. **Legacy JUnit 4 on the Platform:** if you've adopted `useJUnitPlatform()` but still have JUnit 4 tests, you don't switch back to `useJUnit()`. Instead you keep the Platform active and add the **Vintage** engine: ```kotlin tasks.named<Test>("test") { useJUnitPlatform() } dependencies { testImplementation("junit:junit:4.13.2") testRuntimeOnly("org.junit.vintage:junit-vintage-engine") } ``` Vintage is a Platform engine that discovers and runs `org.junit.Test` (JUnit 4) tests, letting Jupiter and JUnit 4 tests coexist in one task. So: TestNG = a distinct selector + framework; legacy JUnit 4 under JUnit 5 = same `useJUnitPlatform()` selector + the Vintage engine on the runtime classpath.

code

kotlin · 8 lines
kotlin
tasks.named<Test>("test") { useJUnitPlatform() }

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
    testImplementation("junit:junit:4.13.2")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

go deeper

for a junior

Know the three selectors exist: useJUnit, useJUnitPlatform, useTestNG.

for a middle

Explain that TestNG needs useTestNG() plus the testng dependency, and that JUnit 5 needs Jupiter on the classpath.

for a senior

Articulate the Vintage-engine path for mixing JUnit 4 and 5 under one Platform task and TestNGOptions like suiteXmlFiles/groups.

for a principal

Plan a staged framework migration across many modules — Vintage as a bridge, deprecation of JUnit 4, and a convention plugin enforcing the target selector.

## Three frameworks, but not three equals The `Test` task selectors are `useJUnit()`, `useJUnitPlatform()`, and `useTestNG()`. They look parallel, but TestNG and the JUnit Platform are architecturally different. ## TestNG path `useTestNG()` switches the task's framework to TestNG and exposes `TestNGOptions` through the configuration closure: ```kotlin tasks.named<Test>("test") { useTestNG { suiteXmlFiles = listOf(file("testng.xml")) includeGroups("smoke") excludeGroups("broken") parallel = "methods" threadCount = 4 } } ``` TestNG runs as its own engine, not through the JUnit Platform. You add `testImplementation("org.testng:testng:7.x")`. Its `includeGroups`/`excludeGroups` are the TestNG analogue of JUnit 5 tags. ## Legacy JUnit 4 under the Platform: the Vintage engine A frequent real-world situation: a project has migrated its build to `useJUnitPlatform()` (so new tests are Jupiter), but a pile of old `org.junit.Test` (JUnit 4) tests remain. You do **not** flip the selector back to `useJUnit()` — that would drop Jupiter discovery. Instead the JUnit Platform supports **multiple engines**, and the **Vintage** engine (`org.junit.vintage:junit-vintage-engine`) is the JUnit 4 compatibility engine: ```kotlin tasks.named<Test>("test") { useJUnitPlatform() } dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testImplementation("junit:junit:4.13.2") // JUnit 4 API for old tests testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } ``` Now a single `test` task runs both Jupiter and JUnit 4 tests, because the Platform launches **all** engines on the classpath. You can even `includeEngines("junit-jupiter")` to temporarily exclude the old ones. ## The decision table - Want TestNG → `useTestNG()` + `org.testng:testng`. - All-new JUnit 5 → `useJUnitPlatform()` + `junit-jupiter`. - Mixed JUnit 5 + legacy JUnit 4 → `useJUnitPlatform()` + `junit-jupiter` + Vintage engine. - Pure legacy JUnit 4, no migration → `useJUnit()` + `junit:junit`. ## Common mistake Switching to `useJUnit()` to 'fix' un-discovered JUnit 4 tests after migrating to the Platform — this silently stops running the Jupiter tests. The right fix is adding Vintage and staying on `useJUnitPlatform()`.

  • Your build migrated to useJUnitPlatform() and now some old JUnit 4 tests don't run. What do you add?
    The Vintage engine: testRuntimeOnly("org.junit.vintage:junit-vintage-engine"). It's the Platform engine that discovers JUnit 4 (@org.junit.Test) tests so they run alongside Jupiter without reverting the selector.
  • What is the TestNG equivalent of JUnit 5 @Tag filtering in the build script?
    useTestNG { includeGroups("smoke"); excludeGroups("broken") } — TestNG groups are filtered through TestNGOptions instead of Platform tags.

saying these in an interview costs you the question

  • Reverting to useJUnit() to run JUnit 4 tests after Platform migration — that drops Jupiter discovery; use Vintage instead.
  • Claiming TestNG runs through the JUnit Platform — it's an independent framework selected by useTestNG().

context