JUnit 4 offered @Test(expected = SomeException.class) and the ExpectedException rule for exception tests. How do you express the same intent in JUnit 5, and why is the replacement considered stronger?
answer
- assertThrows(Type.class, () -> call()) returns the exception
- expected= is method-wide scope; assertThrows is statement-scoped
- ExpectedException needed expect() BEFORE the call — deprecated in 4.13
- assertThrowsExactly for exact class; assertDoesNotThrow for the inverse
- Keep only the call under test inside the lambda
basics
~20 sUse Assertions.assertThrows(SomeException.class, () -> code()), which returns the thrown exception so you can assert its message or cause. It scopes the expectation to one statement, unlike @Test(expected=), which passes if any line in the method throws that type.
solid answer
~50 sBoth JUnit 4 idioms collapse into one Jupiter assertion: ```java IllegalStateException ex = assertThrows( IllegalStateException.class, () -> order.submit()); assertEquals("order already submitted", ex.getMessage()); assertInstanceOf(SQLException.class, ex.getCause()); ``` Why it is better: - **Scope.** `@Test(expected = X.class)` passes if *any* statement in the method throws `X` — including setup or a line before the one you meant. `assertThrows` wraps exactly the call under test. - **The exception is returned**, so asserting on message, cause or custom fields needs no extra machinery. With `expected=` that was impossible; with `ExpectedException` you had to pre-declare matchers. - **No ordering trap.** `ExpectedException` required calling `thrown.expect(...)` *before* the throwing statement; putting it after meant the test failed confusingly. - **Subtype behaviour is explicit**: `assertThrows` accepts subclasses of the declared type; `assertThrowsExactly` demands the exact class. If the exception check is not the point of the test, `assertDoesNotThrow` is the counterpart.
code
java · 16 lines// JUnit 4
@Rule public ExpectedException thrown = ExpectedException.none();
@Test public void rejectsDuplicateSubmit() {
thrown.expect(IllegalStateException.class);
thrown.expectMessage("already submitted");
order.submit();
}
// JUnit 5
@Test
void rejectsDuplicateSubmit() {
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> order.submit());
assertTrue(ex.getMessage().contains("already submitted"));
}go deeper
Know assertThrows(Type.class, lambda) and that it replaces both JUnit 4 idioms.
Explain the scoping advantage and that the exception is returned for further assertions.
Discuss assertThrowsExactly versus subtype matching, brittle message assertions, and the review heuristic of one statement per lambda.
Frame it as contract design: what the test really pins down — type, error code, cause chain — and how that survives refactoring and localisation.
## The three JUnit 4 idioms and their problems **1. `@Test(expected = X.class)`** — the terse form. Its flaw is scope: the assertion is "somewhere in this method an `X` escaped". A test intending to prove that `submit()` rejects a duplicate can pass because the *fixture* threw `IllegalStateException` for an unrelated reason, so the code under test may never even have been called. It also cannot inspect the exception at all. **2. try/fail/catch** — the manual form: ```java try { order.submit(); fail("expected IllegalStateException"); } catch (IllegalStateException expected) { assertEquals("...", expected.getMessage()); } ``` Correct, but verbose, and the failure mode when someone forgets the `fail(...)` line is a test that passes when nothing throws — silently asserting nothing. **3. `ExpectedException` rule** — declarative: ```java @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void rejects() { thrown.expect(IllegalStateException.class); thrown.expectMessage("already submitted"); order.submit(); } ``` Expectations must be declared **before** the throwing call, which reads backwards, and any statement after the throwing call is unreachable — so post-assertions are impossible. Declaring the expectation after the call yields a puzzling failure. The rule was deprecated in JUnit 4.13 in favour of `Assert.assertThrows`, which JUnit 4.13 added precisely because the Jupiter design proved better. ## The Jupiter replacement ```java static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable) ``` `Executable` is a functional interface whose `execute()` may throw anything, so a lambda containing checked-exception calls compiles without wrapping. The method: - fails if nothing is thrown, - fails if something of the wrong type is thrown (and reports what it actually got), - otherwise **returns** the exception, typed. That return value is the whole ergonomic win: assertions about the exception are ordinary assertions after the fact, in normal reading order. ```java var ex = assertThrows(ResponseStatusException.class, () -> controller.get("missing")); assertEquals(404, ex.getStatusCode().value()); assertTrue(ex.getMessage().contains("missing")); ``` ### Exact type versus subtypes `assertThrows(RuntimeException.class, ...)` is satisfied by any subclass — often what you want, sometimes too loose. `assertThrowsExactly` requires the precise class. Prefer the exact form when the *type itself* is the contract (for instance, distinguishing `IllegalArgumentException` from a custom subclass callers switch on). ### Related assertions - `assertDoesNotThrow(executable)` — makes "this must not blow up" explicit and, on failure, reports the exception rather than letting it propagate as an error. - `assertAll(...)` — groups independent assertions so all failures are reported, useful when replacing JUnit 4's `ErrorCollector` rule during the same migration. - `assertTimeout` / `@Timeout` — replaces `@Test(timeout = ...)`. ## Migration mechanics Converting `@Test(expected = X.class)`: 1. Remove the attribute from `@Test`. 2. Identify the *single* statement that should throw — this is the step that finds bugs, because it forces you to say which line you meant. 3. Wrap it in `assertThrows`, and move any setup out of the lambda. 4. Add message/cause assertions if the old test had `ExpectedException.expectMessage`. Converting `ExpectedException`: 1. Delete the `@Rule` field. 2. Turn `thrown.expect(X.class)` into the first argument, `thrown.expectMessage(s)` into an assertion on `ex.getMessage()`, `thrown.expectCause(matcher)` into an assertion on `ex.getCause()`. 3. Put the throwing call in the lambda, keeping it minimal. A useful review rule: the lambda should contain **only** the call under test. If a lambda has three statements, the test has drifted back to the `expected=` problem where any of the three could be the thrower. ## Pitfalls - Wrapping too much code in the lambda reintroduces the scoping weakness. - Ignoring the return value throws away the main benefit; asserting on the message is usually the difference between "it failed" and "it failed for the right reason". - Asserting on message text alone is brittle when messages are user-facing or localised — assert on type plus a stable field or error code where one exists. - `assertThrowsExactly` is not a drop-in for `assertThrows`: switching to it can break tests that legitimately throw subclasses.
- Why is @Test(expected = IllegalStateException.class) considered risky beyond verbosity?Its scope is the entire method, so a fixture line or an early statement that throws the same type makes the test pass without ever exercising the behaviour under test. It also cannot inspect the exception, so a test can be green while the exception carries a completely wrong message or cause.
- What is the difference between assertThrows and assertThrowsExactly?assertThrows accepts the declared type or any subclass, which is usually what you want when the hierarchy is part of the contract. assertThrowsExactly requires exactly the declared class and fails on subclasses. Use the exact form when callers distinguish between a base exception and a specific subtype.
saying these in an interview costs you the question
- Wrapping the whole test body in the assertThrows lambda, recreating the method-wide scope of expected=
- Converting to try/catch without a fail() call, so a non-throwing run passes silently
- Believing ExpectedException expectations can be declared after the throwing statement
- Assuming assertThrows requires the exact exception class (it accepts subclasses)
- Asserting only that something was thrown and never checking message, cause or error code