skip to content

JUnit 4 vs JUnit 5

What changed in JUnit 5: a modular Platform/Jupiter/Vintage architecture, renamed lifecycle annotations, extensions replacing runners and rules, and assertThrows replacing the expected attribute. Still asked because plenty of codebases straddle both versions.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

Describe the three-part architecture of JUnit 5 (Platform, Jupiter, Vintage) and why it was split that way.

level: middleimportance: must knowfreq 55%

basics

~20 s

JUnit 5 is three pieces: the Platform (launches tests and lets build tools/IDEs run them), Jupiter (the new way to write and run tests), and Vintage (an engine that runs your old JUnit 4 tests). JUnit 4 was a single library with no such separation.

open as a page

How do JUnit 5 assertions and exception testing differ from JUnit 4's, including the assertion message and expected-exception handling?

level: middleimportance: should knowfreq 50%

basics

~20 s

JUnit 5 assertions live in org.junit.jupiter.api.Assertions, the optional message moved to the last argument (it was first in JUnit 4), and you test exceptions with assertThrows(...) instead of @Test(expected=...). JUnit 5 also adds assertAll and assertThrows returning the caught exception.

open as a page

How does JUnit 5's extension model (@ExtendWith) differ from JUnit 4's @RunWith and Rules, and why is it considered an improvement?

level: seniorimportance: should knowfreq 45%

basics

~20 s

JUnit 4 customized test behavior with @RunWith (only one runner per class) and @Rule objects. JUnit 5 replaces both with extensions registered via @ExtendWith, and you can apply as many as you want, composing them freely.

open as a page

You inherit a large codebase with thousands of JUnit 4 tests. How do you adopt JUnit 5 incrementally, and what are the trade-offs of the Vintage bridge?

level: principalimportance: should knowfreq 35%

basics

~20 s

Add the JUnit 5 dependencies including the Vintage engine, so old JUnit 4 tests keep running while you write all new tests in JUnit 5. Then migrate old tests gradually, removing Vintage once they're all converted.

open as a page