skip to content

What are the main annotation differences between JUnit 4 and JUnit 5, and how would you migrate a simple test class?

level: juniorimportance: must knowfreq 70%

answer

  1. Each = per test, All = per class
  2. @Ignore → @Disabled
  3. package org.junit.jupiter.api
  4. no more public requirement
  5. expected/timeout attribute gone → assertThrows

basics

~10 s

JUnit 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 s

The 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
java
// 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

for a junior

Recall the four renames plus @Ignore→@Disabled and the new package; recognize a Jupiter test by its imports.

for a middle

Migrate a class cleanly: fix imports, drop public, convert expected/timeout to assertThrows/assertTimeout, move the assertion message to the last argument.

for a senior

Explain why the names changed (explicit per-test vs per-class scope) and why @BeforeAll is static by default (PER_METHOD instance lifecycle).

for a principal

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)

context