What does JUnit 5's @EnableRuleMigrationSupport annotation do, which JUnit 4 rule types can it handle, and what should replace those rules once the migration is finished?
answer
- Artifact: junit-jupiter-migrationsupport; needs JUnit 4 present
- Composite of ExternalResourceSupport + VerifierSupport + ExpectedExceptionSupport
- Covers ExternalResource/TemporaryFolder, Verifier/ErrorCollector, ExpectedException only
- Generic TestRule/MethodRule NOT supported — ignored silently
- Destinations: @TempDir, assertThrows, assertAll, TestInfo, @Timeout, @RegisterExtension
basics
~20 sFrom the junit-jupiter-migrationsupport artifact, it registers extensions that let a Jupiter test still honour JUnit 4 rules of three families: ExternalResource (including TemporaryFolder), Verifier (including ErrorCollector), and ExpectedException. Arbitrary TestRule or MethodRule implementations are not supported. It is a bridge, not a destination.
solid answer
~40 s`@EnableRuleMigrationSupport` lives in `org.junit.jupiter.migrationsupport.rules`, shipped in the **`junit-jupiter-migrationsupport`** artifact, and requires JUnit 4 on the classpath. It is a composite of three extensions: - `ExternalResourceSupport` — rules extending `ExternalResource` (so `TemporaryFolder` too), - `VerifierSupport` — rules extending `Verifier` (so `ErrorCollector`), - `ExpectedExceptionSupport` — the `ExpectedException` rule. Anything else — a hand-written `TestRule` or `MethodRule`, `@RunWith`-based runners, `Timeout`, `TestName`, third-party rules — is **not** covered and is silently ignored. Use it to unblock a class whose rule is expensive to port, not as an end state. The proper replacements: | JUnit 4 rule | Jupiter replacement | |---|---| | `TemporaryFolder` | `@TempDir` | | `ExpectedException` | `assertThrows` | | `ErrorCollector` | `assertAll` | | `TestName` | `TestInfo` parameter | | `Timeout` | `@Timeout` | | custom `ExternalResource` | an `Extension` with `BeforeEachCallback`/`AfterEachCallback` |
code
java · 18 lines// Temporary bridge: keeps the JUnit 4 rule working under Jupiter
@EnableRuleMigrationSupport
class LegacyFileTest {
@Rule public TemporaryFolder folder = new TemporaryFolder();
@Test void writesFile() throws Exception {
File f = folder.newFile("out.txt");
assertTrue(f.exists());
}
}
// Destination: no JUnit 4 involved
class FileTest {
@Test void writesFile(@TempDir Path dir) throws Exception {
Path f = Files.createFile(dir.resolve("out.txt"));
assertTrue(Files.exists(f));
}
}go deeper
Know that Jupiter ignores @Rule and that a separate migration-support artifact bridges a few rule types.
Name the three supported hierarchies, the JUnit 4 classpath requirement, and the first-class replacements like @TempDir and assertThrows.
Decide when the bridge earns its keep versus porting the rule to an Extension, and treat its presence as tracked debt with a removal criterion.
Sequence the work: which shared rules become Extensions owned centrally, which classes may lean on the bridge temporarily, and what makes the migration verifiably finished.
## What a JUnit 4 rule was A *rule* was JUnit 4's composable extension point: a field annotated `@Rule` (per test method) or `@ClassRule` (per class) whose type implements `TestRule` (or the older `MethodRule`). It wrapped the test statement, letting it run code before and after, catch exceptions, or skip execution entirely. `TemporaryFolder`, `ExpectedException`, `ErrorCollector`, `Timeout` and `TestName` were the built-ins; teams wrote their own for database setup, WireMock servers, and so on. Jupiter has no rules. Its extension model is the `Extension` interface with fine-grained callbacks (`BeforeEachCallback`, `AfterEachCallback`, `ParameterResolver`, `TestExecutionExceptionHandler`, …) registered with `@ExtendWith` or `@RegisterExtension`. So a Jupiter class that still declares `@Rule` gets nothing — the field is ignored without warning. ## What migration support provides The `junit-jupiter-migrationsupport` artifact bridges a *limited* subset: - **`ExternalResourceSupport`** handles `@Rule`/`@ClassRule` fields and methods whose type is a subclass of `org.junit.rules.ExternalResource`. That covers `TemporaryFolder` and the very common home-grown `before()`/`after()` resources. - **`VerifierSupport`** handles subclasses of `org.junit.rules.Verifier`, notably `ErrorCollector`, running the verification after the test. - **`ExpectedExceptionSupport`** handles the `ExpectedException` rule. `@EnableRuleMigrationSupport` on a class is simply a composed annotation registering all three; you can also register just the one you need with `@ExtendWith(ExternalResourceSupport.class)`. Hard limits to state plainly: - Only those three hierarchies. A generic `TestRule` or `MethodRule` implementation — including most third-party rules that do not extend `ExternalResource` — is **not** supported and is ignored silently. - JUnit 4 must be on the test classpath, since the rule classes come from it. So the module cannot drop its JUnit 4 dependency while it relies on this. - The support is a compatibility layer with its own semantics; edge behaviours of a rule (statement rewriting, retry logic, exception swallowing) may not translate. ## The proper destinations | JUnit 4 | Jupiter | Notes | |---|---|---| | `TemporaryFolder` | `@TempDir` on a `Path`/`File` parameter or field | built in, cleaned up automatically, configurable cleanup mode | | `ExpectedException` | `assertThrows` | returns the exception for message/cause assertions | | `ErrorCollector` | `assertAll(...)` | reports all failures in a group | | `TestName` | `TestInfo` injected parameter | also gives tags and display name | | `Timeout` | `@Timeout` or `assertTimeout` | per method/class or per block | | `TestWatcher` rule | `TestWatcher` extension interface | direct analogue | | custom `ExternalResource` | `Extension` implementing `BeforeEachCallback`/`AfterEachCallback` | use `@RegisterExtension` when the instance needs constructor arguments | The `@RegisterExtension` form is the closest analogue to a rule *field*: you instantiate the extension yourself, so it can take configuration, and a `static` field gives class-level scope (the `@ClassRule` equivalent) while an instance field gives per-method scope. ## When to use the bridge at all It is worth it when a rule is widely used, non-trivial, and the migration must proceed anyway — you flip the classes to Jupiter now and port the rule later. It is not worth it when the rule has a first-class Jupiter replacement: converting `TemporaryFolder` to `@TempDir` is usually a smaller diff than adding migration support and leaves no JUnit 4 dependency behind. The risk of leaning on it is that it hides an unfinished migration. The module still depends on JUnit 4, the tests read as a hybrid, and new readers cannot tell which mechanism is authoritative. Treat any use as debt with an owner: track the classes that carry `@EnableRuleMigrationSupport`, and make removing it the definition of done for that module. ## Version note The annotation and the artifact belong to JUnit Jupiter 5.x and depend on JUnit 4.12+ being present. Its scope has never widened beyond the three rule hierarchies, so do not plan a migration around support that might arrive for arbitrary `TestRule`s.
- You have a home-grown @Rule that starts an embedded HTTP server and implements TestRule directly. Does migration support cover it?No. Only rules extending ExternalResource, extending Verifier, or being ExpectedException are bridged; a direct TestRule implementation is ignored with no warning. Either refactor the rule to extend ExternalResource so the bridge applies, or — better — rewrite it as an Extension with BeforeEachCallback/AfterEachCallback and register it with @RegisterExtension so it can still take constructor arguments.
- What is the Jupiter equivalent of a @ClassRule field?A static field annotated @RegisterExtension holding an extension instance, or @ExtendWith on the class when no configuration is needed. A static @RegisterExtension field gives class-level scope with BeforeAllCallback/AfterAllCallback semantics, while a non-static one is per test method, mirroring the @ClassRule versus @Rule distinction.
saying these in an interview costs you the question
- Believing @EnableRuleMigrationSupport makes any JUnit 4 rule work under Jupiter
- Assuming the module can drop its JUnit 4 dependency while still using rule migration support
- Using the bridge for TemporaryFolder instead of the built-in @TempDir
- Expecting a warning or error when an unsupported rule is present — it is silently ignored
- Treating migration support as a permanent architecture rather than temporary debt