What are the main annotation differences between JUnit 4 and JUnit 5, and how would you migrate a simple test class?
answer
- Each = per test, All = per class
- @Ignore → @Disabled
- package org.junit.jupiter.api
- no more public requirement
- expected/timeout attribute gone → assertThrows
basics
~10 sJUnit 5 renames the lifecycle annotations: @Before becomes @BeforeEach, @After becomes @AfterEach, @BeforeClass becomes @BeforeAll, @AfterClass becomes @AfterEach's partner @AfterAll, and @Ignore becomes @Disabled. They also move to the org.junit.jupiter.api package.
solid answer
~30 sThe most visible change is renamed annotations plus a new package. JUnit 4's @Before/@After run around every test and become @BeforeEach/@AfterEach; @BeforeClass/@AfterClass (which ran once per class and had to be static) become @BeforeAll/@AfterAll. @Ignore becomes @Disabled, and @Test moves from org.junit to org.junit.jupiter.api (and no longer takes expected/timeout attributes). To migrate, you change imports from org.junit.* to org.junit.jupiter.api.*, rename the four lifecycle annotations and @Ignore, drop public visibility requirements on test methods, and replace expected exceptions with assertThrows. The new names read better and make the per-test vs per-class distinction explicit.
code
java · 21 lines// JUnit 4
import org.junit.*;
import static org.junit.Assert.*;
public class CartTest {
@BeforeClass public static void boot() {}
@Before public void setUp() {}
@Ignore @Test(expected = IllegalStateException.class)
public void empties() {}
}
// JUnit 5 (Jupiter)
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CartTest {
@BeforeAll static void boot() {}
@BeforeEach void setUp() {}
@Disabled @Test
void empties() {
assertThrows(IllegalStateException.class, () -> cart.checkout());
}
}go deeper
Recall the four renames plus @Ignore→@Disabled and the new package; recognize a Jupiter test by its imports.
Migrate a class cleanly: fix imports, drop public, convert expected/timeout to assertThrows/assertTimeout, move the assertion message to the last argument.
Explain why the names changed (explicit per-test vs per-class scope) and why @BeforeAll is static by default (PER_METHOD instance lifecycle).
Frame the rename as part of a deliberate API redesign that decoupled the runner from the model, and weigh mechanical migration vs running JUnit 4 tests under Vintage during a phased move.
## What JUnit is JUnit is the standard **unit-testing framework** for Java: you write small methods that exercise a piece of code and *assert* the expected result, and a test *runner* executes them and reports pass/fail. A **test class** is an ordinary Java class whose methods are marked with annotations the framework recognizes. ## The two generations - **JUnit 4** (2006) — everything lives in the `org.junit` package; tests are marked `@Test` from `org.junit.Test`. - **JUnit 5** (2017), internally called **Jupiter** — a rewrite with a new programming model in the `org.junit.jupiter.api` package. ## Lifecycle methods A *lifecycle method* is code that runs around your tests for setup/teardown (e.g. opening a database connection, clearing a list). There are two scopes: - **Per-test** — runs before/after *each* test method, giving every test a fresh state. - **Per-class** — runs *once* for the whole class (expensive shared setup). ## The renames (the heart of this topic) | JUnit 4 | JUnit 5 | Meaning | |---|---|---| | `@Before` | `@BeforeEach` | run before every test | | `@After` | `@AfterEach` | run after every test | | `@BeforeClass` | `@BeforeAll` | run once before all tests | | `@AfterClass` | `@AfterAll` | run once after all tests | | `@Ignore` | `@Disabled` | skip this test | | `@Test` (org.junit) | `@Test` (org.junit.jupiter.api) | mark a test (same name, different package) | The JUnit 4 names were vague: `@Before` didn't say *before what*. JUnit 5 makes the scope explicit (`Each` vs `All`). ## Other migration-relevant changes - **Visibility:** JUnit 4 required test methods (and the class) to be `public`. JUnit 5 only requires *package-private or higher* — `public` is no longer needed (but private/static-for-@Test is still rejected). - **`@Test` attributes gone:** JUnit 4 let you write `@Test(expected = X.class)` and `@Test(timeout = 100)`. In JUnit 5 those attributes don't exist — you use `assertThrows(X.class, () -> …)` and `assertTimeout(…)` (or the `@Timeout` annotation) instead. - **Assertions package:** static assert methods move from `org.junit.Assert` to `org.junit.jupiter.api.Assertions`, and the optional *message* argument moved to the **last** parameter (JUnit 4 had it first). - **`@BeforeAll`/`@AfterAll`** must be `static` by default (same as JUnit 4's class-level ones), unless you switch the test instance lifecycle to `PER_CLASS`. ## A migration in practice Given a JUnit 4 class, you: (1) swap `import org.junit.*` for `import org.junit.jupiter.api.*` and `static org.junit.jupiter.api.Assertions.*`; (2) rename the four lifecycle annotations and `@Ignore`; (3) remove `public`; (4) convert `@Test(expected=…)`/`timeout` to `assertThrows`/`assertTimeout`. The code logic is unchanged — it's mostly mechanical, which is why tools and the Vintage engine (so you don't have to migrate all at once) exist.
- Why does @BeforeAll have to be static by default?Because it runs once before any test instance is created — JUnit 5 builds a new test instance per test method by default (PER_METHOD lifecycle), so there's no instance to call it on. Switching to @TestInstance(PER_CLASS) lets it be non-static.
- In JUnit 5 assertions, where did the message parameter go?It moved from the first argument (JUnit 4: assertEquals(message, expected, actual)) to the last (JUnit 5: assertEquals(expected, actual, message)), and it can be a lazily-evaluated Supplier<String>.
JUnit 4's @Before/@BeforeClass are like a sign that just says 'prep'; JUnit 5's @BeforeEach/@BeforeAll add the missing word — 'prep each guest' vs 'prep the whole venue once'.
saying these in an interview costs you the question
- Thinking @Before maps to @BeforeAll (it maps to @BeforeEach)
- Believing test methods still must be public in JUnit 5
- Saying @Test(expected=...) still works in Jupiter — it was removed in favor of assertThrows
- Assuming the package stayed org.junit (it's org.junit.jupiter.api)