When moving a test class from JUnit 4 to JUnit 5, what are the JUnit 5 equivalents of @Before, @BeforeClass, @After, @AfterClass, @Ignore and @Category, and what changes besides the names?
answer
- Before→BeforeEach, BeforeClass→BeforeAll, Ignore→Disabled, Category→Tag
- @Test has no attributes: expected→assertThrows, timeout→@Timeout
- Assertions message moves to LAST parameter
- Not public required, but must not be private
- @BeforeAll static unless @TestInstance(PER_CLASS)
basics
~10 s@Before becomes @BeforeEach, @BeforeClass becomes @BeforeAll, @After becomes @AfterEach, @AfterClass becomes @AfterAll, @Ignore becomes @Disabled, @Category(X.class) becomes @Tag("x"). Packages change to org.junit.jupiter.api, assertions move to Assertions, and the message argument moves to last.
solid answer
~50 sThe name mapping: | JUnit 4 | JUnit 5 (Jupiter) | |---|---| | `@Before` / `@After` | `@BeforeEach` / `@AfterEach` | | `@BeforeClass` / `@AfterClass` | `@BeforeAll` / `@AfterAll` | | `@Ignore` | `@Disabled("reason")` | | `@Category(Slow.class)` | `@Tag("slow")` | | `org.junit.Test` | `org.junit.jupiter.api.Test` | But the imports are the smaller half: - **`@Test` has no attributes.** `expected = X.class` becomes `assertThrows(X.class, () -> …)`; `timeout = 500` becomes `assertTimeout(...)` or `@Timeout(500, MILLISECONDS)`. - **Assertions move** from `org.junit.Assert` to `org.junit.jupiter.api.Assertions`, and the optional message becomes the **last** parameter instead of the first. - **Assumptions** move from `org.junit.Assume` to `org.junit.jupiter.api.Assumptions`. - **Visibility**: test classes and methods no longer need to be `public`, but must not be `private`. - **`@BeforeAll`/`@AfterAll` must be `static`** unless the class is `@TestInstance(PER_CLASS)`. - `@RunWith` is gone — its replacement is `@ExtendWith`. - Categories were marker *classes*; tags are *strings*, so a marker interface hierarchy does not translate one-to-one.
code
java · 19 lines// JUnit 4
public class OrderTest {
@Before public void setUp() {}
@BeforeClass public static void once() {}
@Ignore("flaky")
@Test(expected = IllegalStateException.class)
public void rejects() { new Order().submit(); }
}
// JUnit 5 (Jupiter)
class OrderTest {
@BeforeEach void setUp() {}
@BeforeAll static void once() {}
@Disabled("flaky")
@Test
void rejects() {
assertThrows(IllegalStateException.class, () -> new Order().submit());
}
}go deeper
Recite the annotation mapping accurately and know the package change to org.junit.jupiter.api.
Add the behavioural deltas: @Test attributes gone, assertion message last, static @BeforeAll, non-private visibility.
Emphasise the silent failures — ignored @Rule/@RunWith, private methods not discovered, the three-string assertEquals trap — and how you would catch them in review.
Treat the mapping as mechanical work for tooling, and focus on the review gate and static checks that prevent a green-but-not-running corpus.
## The straightforward renames JUnit 4's lifecycle annotations were named for *when* they ran relative to the class; Jupiter's are named for *what* they run around, which reads better: - `@Before` → `@BeforeEach`, `@After` → `@AfterEach` — run around every test method. - `@BeforeClass` → `@BeforeAll`, `@AfterClass` → `@AfterAll` — run once for the class. - `@Ignore` → `@Disabled`. Jupiter takes an optional reason string and shows it in reports, which is worth filling in. - `@Category(SlowTests.class)` → `@Tag("slow")`. - `@Test` itself changes package: `org.junit.Test` → `org.junit.jupiter.api.Test`. All of these are imports plus a rename, and automated tooling handles them well. ## The changes that alter behaviour ### `@Test` lost its attributes JUnit 4's `@Test(expected = IllegalStateException.class)` and `@Test(timeout = 500)` do not exist in Jupiter. Exceptions become `assertThrows`, which returns the thrown exception so you can assert on its message or cause. Timeouts become `@Timeout(value = 500, unit = MILLISECONDS)` on the method/class, or `assertTimeout`/`assertTimeoutPreemptively` around a specific block. This is not cosmetic: `expected=` passed if *anything anywhere* in the method threw that type, including the setup, whereas `assertThrows` scopes the expectation to one statement. ### Assertion signatures flipped `org.junit.Assert.assertEquals(String message, Object expected, Object actual)` puts the message first. `org.junit.jupiter.api.Assertions.assertEquals(Object expected, Object actual, String message)` puts it last. Most mis-migrations fail to compile, but the three-string case (`assertEquals("msg", "a", "b")`) compiles under both with completely different meanings — a real, silent defect. Jupiter also adds a `Supplier<String>` overload so the message is only built on failure. Jupiter drops `assertThat` entirely; if the class used Hamcrest matchers, keep Hamcrest (or move to AssertJ) as a separate dependency — Jupiter deliberately does not ship a matcher library. ### Assumptions `org.junit.Assume.assumeTrue` → `org.junit.jupiter.api.Assumptions.assumeTrue`. Semantics are similar (a failed assumption aborts rather than fails the test), and Jupiter adds `assumingThat(condition, executable)` to run only part of a test conditionally. ### Visibility and `static` JUnit 4 required `public` test classes and `public void` test methods. Jupiter uses different reflection rules: classes and methods must simply not be `private`, so package-private is idiomatic. A method left `private` during migration is **not discovered** — silently no longer running. `@BeforeAll`/`@AfterAll` must be `static` because, by default, Jupiter creates a **new test instance per test method** (`Lifecycle.PER_METHOD`), so there is no single instance to hold them. Annotate the class `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` to get one instance per class and then non-static `@BeforeAll` is allowed. That choice also means fields survive between test methods, which is exactly the shared-state behaviour JUnit 4 users often assumed and which per-method lifecycle prevents. ### `@RunWith` and rules Jupiter has no runners. `@RunWith(MockitoJUnitRunner.class)` becomes `@ExtendWith(MockitoExtension.class)`, `@RunWith(SpringRunner.class)` becomes `@ExtendWith(SpringExtension.class)` (usually implied by `@SpringBootTest`). Unlike runners, extensions compose: several `@ExtendWith` on one class is fine, which was the single biggest limitation of the runner model. Any leftover `@RunWith` or `@Rule` in a Jupiter class is simply ignored — no error, no warning that stops the build. ### Categories versus tags `@Category` took marker *classes*, which supported inheritance: a category interface extending another matched both. `@Tag` takes *strings* with no hierarchy. Migrating a category hierarchy means flattening it — a test that was in a sub-category may need two tags. Tag strings must not be blank or contain whitespace, commas, or the characters `(`, `)`, `&`, `|`, `!`, since those are tag-expression operators. ## Practical advice Run an automated migration (IDE action or a recipe-based tool) for the mechanical renames, then review every diff for the four behavioural traps: `expected=`/`timeout=` conversions, assertion argument order, `static` on `@BeforeAll`, and any surviving `@RunWith`/`@Rule`/`@Ignore` from the JUnit 4 packages. Those four are where converted-and-green does not mean converted-and-correct.
- Why must @BeforeAll be static by default, and how can it not be?Jupiter's default lifecycle is PER_METHOD: a fresh test instance is created for every test method, so there is no single instance on which a once-per-class method could run — hence static. Annotating the class @TestInstance(Lifecycle.PER_CLASS) creates one instance for the whole class, and then @BeforeAll and @AfterAll may be non-static. That also makes instance fields persist across test methods.
- What is the risk of leaving a @Rule field in a class you converted to Jupiter?Jupiter does not know what a JUnit 4 @Rule is, so the field is ignored with no error. Whatever setup, teardown or assertion the rule provided silently stops happening, and the test can still pass for the wrong reason. Either replace it with an Extension (or a built-in like @TempDir), or use the migration-support extensions for the supported rule types.
saying these in an interview costs you the question
- Believing Jupiter test classes and methods must still be public
- Converting @Test(expected = X.class) to a try/catch with no fail() call, so a non-throwing case passes
- Leaving the assertion message as the first argument after switching to Assertions
- Keeping @RunWith or @Rule in a Jupiter class and assuming they still work
- Thinking @Category marker classes map one-to-one onto @Tag strings, including inheritance