What is JUnit 5's assertThrows and how do you use it to verify that code throws an expected exception?
answer
- Assertions.assertThrows(Type.class, executable)
- Executable = lambda/method ref, deferred run
- Passes on type OR subtype
- Returns the caught exception
- Lambda defers so JUnit can intercept the throw
basics
~20 sassertThrows runs a piece of code and checks it throws the exception type you expect. You pass the expected exception class and a lambda holding the code. The test passes only if that code throws that type (or a subtype).
solid answer
~40 sassertThrows is a static assertion in org.junit.jupiter.api.Assertions. You call assertThrows(ExpectedException.class, executable), where the executable is a lambda or method reference wrapping the code under test. JUnit invokes it; if it throws an instance of the expected type (or a subtype), the assertion passes and the caught exception is returned so you can inspect it. If nothing is thrown, or a different (non-assignable) type is thrown, the test fails. It replaced JUnit 4's clumsier mechanisms by scoping the expectation to exactly the lines you wrap, and by handing back the exception object for further assertions on its message, cause, or fields. A typical use captures the return value: var ex = assertThrows(IllegalArgumentException.class, () -> service.parse("")); then asserts on ex.getMessage().
code
java · 14 linesimport static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class ParserTest {
@Test
void rejectsEmptyInput() {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> Parser.parse("") // executable: deferred until JUnit runs it
);
assertEquals("empty input", ex.getMessage());
}
}go deeper
Knows assertThrows(Type.class, lambda) is how you test that code throws; can write a basic case and explain the lambda holds the code under test.
Captures the returned exception and asserts on its message/cause; knows it matches subtypes and uses the message overload for clarity.
Articulates the subtype-vs-exact-type semantics, contrasts with assertThrowsExactly, and explains why it superseded JUnit 4's @Test(expected) and ExpectedException rule.
Sets team conventions: narrow exception assertions, asserting messages/causes without brittleness, and guidance on when exact-type matching matters for API contracts.
## What is an exception assertion? A **unit test** checks that a small piece of code behaves correctly. Sometimes the *correct* behavior is to **throw an exception** — for example, calling `Integer.parseInt("abc")` should throw `NumberFormatException`. To test that, you need a way to say: "running this code MUST throw THIS kind of exception, and if it does not, fail the test." **JUnit 5** (the testing framework, also called *JUnit Jupiter*) provides this via the static method **`assertThrows`**, found in the class `org.junit.jupiter.api.Assertions`. ## The signature ``` static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable) ``` Two arguments: - **`expectedType`** — the `Class` object of the exception you expect, written as `SomeException.class`. - **`executable`** — a functional interface `org.junit.jupiter.api.function.Executable` with one method `void execute() throws Throwable`. Because it is a *functional interface*, you supply it as a **lambda** `() -> ...` or a **method reference**. This is the actual code under test. *(A **functional interface** is an interface with exactly one abstract method, so a lambda can implement it. A **lambda** `() -> expr` is a short inline function.)* ## What it does, step by step 1. JUnit calls `executable.execute()` — i.e. it runs your wrapped code. 2. If that code throws a `Throwable` that **is an instance of** `expectedType` (the exact type *or any subclass* of it), the assertion **succeeds** and `assertThrows` **returns** that caught exception object (typed as `T`). 3. If the code throws a different type that is *not* assignable to `expectedType`, the assertion **fails** (and JUnit reports the unexpected exception). 4. If the code throws **nothing at all**, the assertion **fails** with a message like "Expected X to be thrown, but nothing was thrown." ## Why wrap code in a lambda? The lambda **defers execution**. The exception must be thrown *inside* `assertThrows`'s control so it can catch it. If you wrote the failing call directly, it would throw before `assertThrows` ever ran. The lambda packages the code so JUnit decides *when* to run it and can intercept the throw. ## Returning the exception A key feature: the method **returns the caught exception**, so you can keep asserting on it: ``` IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> parse("")); assertEquals("empty input", ex.getMessage()); ``` This lets you verify not just *that* it threw, but the **message**, **cause**, or any custom field. ## Optional message overload There is an overload with a third argument — a `String` or `Supplier<String>` failure message — shown when the assertion fails, e.g. `assertThrows(X.class, exec, "parse should reject empty input")`. ## Summary `assertThrows(Type.class, () -> codeThatShouldThrow())` is the standard JUnit 5 way to assert an exception. It passes on the type **or a subtype**, returns the exception for deeper checks, and scopes the expectation to exactly the wrapped lines.
- What happens if the wrapped code throws no exception at all?The assertion fails with a message like 'Expected IllegalArgumentException to be thrown, but nothing was thrown.' Throwing nothing is a failure, not a pass.
- Why must the code under test go inside a lambda?The lambda defers execution so assertThrows controls when the code runs and can catch the throw. A direct call would throw before assertThrows could intercept it.
It is like a bomb-disposal box: you put the suspicious code inside, JUnit sets it off in a contained space, and hands you back exactly which exception 'exploded' so you can examine it.
saying these in an interview costs you the question
- Thinking the code can be called directly instead of inside the executable lambda — it would throw before assertThrows runs.
- Believing assertThrows only passes on the exact type — it also passes on subtypes.
- Forgetting that you can (and often should) capture and assert on the returned exception.
- Confusing JUnit 5's Assertions.assertThrows with JUnit 4's @Test(expected=...).