How do JUnit 5 assertions and exception testing differ from JUnit 4's, including the assertion message and expected-exception handling?
answer
- org.junit.Assert → org.junit.jupiter.api.Assertions
- message LAST in JUnit 5 (was first)
- message can be Supplier<String> (lazy)
- assertThrows returns the exception
- assertAll runs & reports all failures
basics
~20 sJUnit 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.
solid answer
~40 sThree concrete differences. First, the **package**: static asserts move from org.junit.Assert to org.junit.jupiter.api.Assertions. Second, the **message argument position**: JUnit 4 put the optional failure message *first* (assertEquals("msg", expected, actual)); JUnit 5 puts it *last* (assertEquals(expected, actual, "msg")) and it can be a Supplier<String> so it's only built on failure. Third, **exception testing**: JUnit 4 used @Test(expected = X.class) or the ExpectedException rule; JUnit 5 removes the attribute and gives you assertThrows(X.class, () -> code()), which returns the thrown exception so you can assert on its message or fields. JUnit 5 also adds assertAll (group assertions so all run and all failures report together) and assertTimeout. The message-position swap is the most common migration bug, because the code still compiles but the message becomes the 'actual' value.
code
java · 20 lines// JUnit 4
import static org.junit.Assert.*;
@Test(expected = IllegalStateException.class)
public void emptyCart() {
assertEquals("sizes differ", 3, list.size()); // message FIRST
cart.checkout();
}
// JUnit 5
import static org.junit.jupiter.api.Assertions.*;
@Test
void emptyCart() {
assertEquals(3, list.size(), "sizes differ"); // message LAST
var ex = assertThrows(IllegalStateException.class, () -> cart.checkout());
assertEquals("cart empty", ex.getMessage()); // can inspect the exception
assertAll(
() -> assertEquals("A", order.getCode()),
() -> assertTrue(order.isPaid()) // both run even if the first fails
);
}go deeper
Know the assertions package changed and that exceptions are tested with assertThrows instead of @Test(expected=...).
Explain the message-last swap (and the silent-compile trap), assertThrows returning the exception, and assertAll/assertTimeout additions.
Discuss lazy Supplier messages, assertThrowsExactly vs assertThrows, and that JUnit 5 unbundles matcher libraries (bring AssertJ/Hamcrest).
Advise on assertion conventions at scale — when to standardize on AssertJ over built-ins, catching message-position regressions in machine-converted migrations, and grouping semantics with assertAll.
## What an assertion is An **assertion** is a check inside a test: `assertEquals(expected, actual)` fails the test if they differ. A failed assertion throws an `AssertionError` the runner reports. Both JUnit versions ship a set of static `assert*` methods. ## Difference 1 — the package - **JUnit 4:** `org.junit.Assert` (`import static org.junit.Assert.*`). - **JUnit 5:** `org.junit.jupiter.api.Assertions` (`import static org.junit.jupiter.api.Assertions.*`). Same familiar names (`assertEquals`, `assertTrue`, `assertNull`, `fail`…), new home. ## Difference 2 — where the optional message goes (the migration trap) Many asserts take an optional human-readable failure message. The two versions order it differently: - **JUnit 4 — message FIRST:** `assertEquals("sizes differ", 3, list.size())`. - **JUnit 5 — message LAST:** `assertEquals(3, list.size(), "sizes differ")`. Why it bites you: if you copy JUnit 4 code into JUnit 5, `assertEquals("sizes differ", 3, list.size())` may still *compile* but now interprets `"sizes differ"` as the *expected* value and `3` as the *actual* — a silent semantic change. JUnit 5 also lets the message be a **`Supplier<String>`** (a lambda), so an expensive message string is only constructed when the assertion actually fails. ## Difference 3 — testing that code throws - **JUnit 4** offered two ways: `@Test(expected = IllegalStateException.class)` (whole-method, can't assert on the exception's contents and can't pinpoint *which* line threw) and the `ExpectedException` **Rule** (more control but verbose, set up *before* the call). - **JUnit 5** removes the `expected` attribute and standardizes on **`assertThrows`**: ```java var ex = assertThrows(IllegalStateException.class, () -> cart.checkout()); assertEquals("cart empty", ex.getMessage()); ``` It runs the lambda, asserts the exact (or subtype) exception type, and **returns** the caught exception so you can make further assertions on its message/cause/fields. There's also `assertThrowsExactly` for an exact type match. ## Bonus JUnit 5 assertion additions - **`assertAll(...)`** — groups several assertions so they *all* execute even if an early one fails, then reports *every* failure at once (instead of stopping at the first). Great for asserting multiple fields of an object. - **`assertTimeout` / `assertTimeoutPreemptively`** — replace JUnit 4's `@Test(timeout = …)` attribute. - **`assertIterableEquals`, `assertLinesMatch`** and friends — richer built-ins. ## Note on third-party matchers JUnit 4 bundled Hamcrest's `assertThat`. JUnit 5 deliberately **does not** bundle a matcher library — you bring your own (Hamcrest or, more commonly, **AssertJ**) for fluent assertions. Jupiter's built-in `Assertions` are intentionally minimal. ## Mental model Think of `assertEquals` arguments as 'value, value, *then* the note'. JUnit 4 put the note first; JUnit 5 puts it last (and makes it lazy). For exceptions, stop annotating the method and instead *wrap the throwing call* in `assertThrows` so you can interrogate what came out.
- Why is moving the assertion message to a Supplier<String> useful?The message string is only built when the assertion fails, so you avoid the cost of constructing an expensive message (e.g. with string concatenation or reflection) on every passing assertion.
- What does assertAll give you that a sequence of plain assertEquals calls doesn't?It executes every grouped assertion even if earlier ones fail and reports all failures together, instead of stopping at the first failure — so one test run shows every problem rather than one at a time.
saying these in an interview costs you the question
- Putting the assertion message first in JUnit 5 (it's last)
- Believing @Test(expected=...) still works in JUnit 5
- Thinking assertThrows returns void — it returns the caught exception
- Assuming JUnit 5 still bundles Hamcrest's assertThat (it doesn't)