How does Surefire choose between JUnit 4, JUnit 5 (Jupiter), and TestNG providers, and how do you wire JUnit 5 correctly?
answer
- provider auto-detected from classpath
- Surefire 2.22+ native JUnit Platform
- junit-jupiter = api+engine
- vintage engine for JUnit4-on-platform
- junit-bom to avoid skew
basics
~20 sSurefire auto-detects the test framework from your test-scoped dependencies and selects a matching provider. For JUnit 5 you add junit-jupiter (API + engine); modern Surefire (2.22+/3.x) discovers the JUnit Platform automatically, so you usually need no extra provider dependency.
solid answer
~40 sSurefire ships providers for JUnit 4, the JUnit Platform (JUnit 5/Jupiter), and TestNG, and picks one by inspecting what's on the test classpath. With JUnit 4 it uses surefire-junit4/junit47; with junit-jupiter present it uses the JUnit Platform provider; with testng it uses surefire-testng. Since Surefire 2.22, JUnit Platform support is built in, so you typically just add the junit-jupiter aggregator (or junit-jupiter-api + junit-jupiter-engine) at test scope and Surefire finds the engine. Pitfalls: mixing JUnit 4 and 5 needs junit-vintage-engine to run old JUnit 4 tests on the Platform; an outdated Surefire version (<2.22) silently runs zero JUnit 5 tests; and version skew between junit-jupiter and the platform can cause 'no tests found'. Pin a modern Surefire (3.x) and a single BOM-managed JUnit 5 version.
code
xml · 13 lines<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<!-- needed only to run legacy JUnit 4 tests -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>go deeper
Know Surefire picks the test framework based on dependencies on the classpath.
Wire JUnit 5 with junit-jupiter + a modern Surefire; know an engine is required.
Diagnose zero-tests/version-skew issues and migrate JUnit 4->5 with the vintage engine.
Standardize a BOM-managed test stack and Surefire version across modules to prevent silent provider mismatches.
## Provider auto-detection Surefire does not hardcode a framework; it scans the **test-scoped** dependencies and chooses a *provider*: - `junit:junit` (4.x) -> `surefire-junit4` / `surefire-junit47` - `org.junit.jupiter:junit-jupiter*` (5.x) -> JUnit Platform provider (`surefire-junit-platform`) - `org.testng:testng` -> `surefire-testng` If multiple are present, the result can be ambiguous; keep one primary framework per module. ## Wiring JUnit 5 From **Surefire 2.22.0** onward (and all 3.x), JUnit Platform discovery is native. Typical setup: ```xml <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <!-- pulls api + engine + params --> <version>5.10.2</version> <scope>test</scope> </dependency> </dependencies> <build><plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> </plugin> </plugins></build> ``` No explicit `<provider>` is needed. ## Running legacy JUnit 4 on the Platform To run old JUnit 4 tests alongside Jupiter, add the **vintage** engine: ```xml <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> </dependency> ``` ## TestNG Add `org.testng:testng` at test scope; Surefire picks `surefire-testng`. You can pass a `suiteXmlFiles` config to drive TestNG suites. ## Common failure modes - **Zero tests run with JUnit 5**: Surefire version is older than 2.22 - upgrade. - **'No tests found' / engine missing**: you added `junit-jupiter-api` but not an engine (`junit-jupiter-engine`); use the `junit-jupiter` aggregator or add the engine explicitly. - **Version skew**: mismatched `junit-jupiter` and `junit-platform` versions - manage via `junit-bom`. - **Provider clash**: JUnit 4 + 5 both present without vintage engine can leave JUnit 4 tests unexecuted. ## Best practice Import the JUnit BOM and pin a recent Surefire 3.x: ```xml <dependencyManagement><dependencies> <dependency> <groupId>org.junit</groupId> <artifactId>junit-bom</artifactId> <version>5.10.2</version> <type>pom</type><scope>import</scope> </dependency> </dependencies></dependencyManagement> ```
- Your JUnit 5 tests are silently skipped (0 run). First thing to check?The Surefire plugin version - anything before 2.22.0 lacks native JUnit Platform support; upgrade to 3.x.
- How do you keep old JUnit 4 tests running after migrating to JUnit 5?Add junit-vintage-engine at test scope so the JUnit Platform runs JUnit 4 tests too.
- Why prefer the junit-jupiter aggregator over just junit-jupiter-api?The aggregator includes the engine (and params); api alone has no engine, so the platform finds no tests to run.
saying these in an interview costs you the question
- Saying you must declare an explicit <provider> for JUnit 5 in modern Surefire.
- Adding only junit-jupiter-api and expecting tests to run.
- Believing Surefire defaults to JUnit 4 regardless of classpath.