JUnit 4 let you write @Test(expected = SomeException.class) and @Test(timeout = 1000). How do you express those in JUnit 5, and why was the change made?
answer
- expected -> assertThrows (scoped lambda, returns exception)
- timeout -> assertTimeout / assertTimeoutPreemptively / @Timeout
- Jupiter @Test has NO elements
- assertThrows lets you inspect message/cause
- preemptively = separate thread, can abort a hang
basics
~10 sJUnit 5's @Test has no expected or timeout attributes. Instead you use assertThrows to check an exception is thrown and assertTimeout (or @Timeout) to bound how long code may run.
solid answer
~40 sJUnit 4's @Test carried two elements: expected, which passed the test if the named exception was thrown anywhere in the method, and timeout, which failed it if the method ran too long. JUnit 5 removed both because they were coarse. expected couldn't pinpoint which statement threw, couldn't inspect the exception's message or cause, and would pass even if the wrong line threw. JUnit 5 replaces it with assertThrows, which scopes the expectation to a specific lambda and returns the caught exception so you can assert on its message or cause. timeout is replaced by assertTimeout (runs to completion then checks elapsed time) and assertTimeoutPreemptively (aborts in another thread when exceeded), plus the declarative @Timeout annotation. The result is more precise, composable assertions instead of overloaded annotation attributes.
code
java · 24 linesimport org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
class MigrationExampleTest {
// JUnit 4: @Test(expected = IllegalArgumentException.class)
@Test
void throwsOnBadInput() {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> Integer.parseInt("oops".substring(0) + "x"));
// can now inspect the exception
assertNotNull(ex.getMessage());
}
// JUnit 4: @Test(timeout = 1000)
@Test
void finishesInTime() {
assertTimeout(Duration.ofSeconds(1), () -> {
// work that must finish within 1s
});
}
}go deeper
Knows JUnit 5 uses assertThrows for exceptions and a timeout assertion/annotation instead of @Test attributes.
Can write assertThrows with a lambda, inspect the returned exception, and use assertTimeout or @Timeout; explains that Jupiter's @Test has no elements.
Articulates why the change improves precision (scoping, inspection), and the assertTimeout vs assertTimeoutPreemptively thread/abort trade-off.
Frames it as a design shift from overloaded annotations to composable assertions/extensions, and weighs preemptive timeouts against context-bound code (ThreadLocal, transactions, resource cleanup).
## The JUnit 4 way In **JUnit 4**, the `@Test` annotation had two optional **elements** (annotation attributes): ```java @Test(expected = IllegalArgumentException.class) public void rejectsBadInput() { parse("not a number"); } @Test(timeout = 1000) // milliseconds public void finishesQuickly() { doWork(); } ``` - **`expected`** made the test *pass* if an exception of that type was thrown **anywhere** in the method body, and *fail* if none was thrown. - **`timeout`** made the test *fail* if the method took longer than the given milliseconds. ## Why these were removed in JUnit 5 Both are **too coarse**: 1. **`expected` can't say *where*.** If the exception could come from setup code rather than the line you mean to test, the test passes for the wrong reason. You also **cannot inspect the exception** — its message, cause, or fields — because you never get a handle to it. 2. It's **all-or-nothing per method.** You can only assert one exception, for the whole method. JUnit 5 favors **assertions you compose in the test body** over **behavior baked into the annotation**. This keeps `@Test` a plain marker and pushes precise behavior into small, explicit calls. ## The JUnit 5 replacements ### `assertThrows` (replaces `expected`) ```java import static org.junit.jupiter.api.Assertions.*; @Test void rejectsBadInput() { IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, () -> parse("not a number")); // only THIS call is expected to throw assertEquals("invalid: not a number", ex.getMessage()); // inspect it } ``` `assertThrows` takes the expected type and a **lambda** (an `Executable`). It fails if the lambda doesn't throw, or throws the wrong type, and otherwise **returns the caught exception** so you can make further assertions on it. There is also `assertThrowsExactly` when you want the exact class, not a subtype. ### `assertTimeout` / `assertTimeoutPreemptively` (replaces `timeout`) ```java import java.time.Duration; @Test void finishesQuickly() { assertTimeout(Duration.ofSeconds(1), () -> doWork()); } ``` - **`assertTimeout`** lets the code **run to completion**, then fails if it took too long. The code runs on the same thread. - **`assertTimeoutPreemptively`** runs the code on a **separate thread** and **aborts** it the moment the limit is exceeded — useful when the code could hang forever, but be careful with code that relies on `ThreadLocal` or transactions, since it runs on a different thread. ### `@Timeout` (declarative form) ```java @Test @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) void mustBeFast() { doWork(); } ``` `@Timeout` can also annotate the class or `@BeforeEach`/`@AfterEach` to bound them. ## Takeaway JUnit 5's `@Test` is a **bare marker with no elements**. Exception expectations and timeouts moved into **explicit, scoped assertions** (`assertThrows`, `assertTimeout`) and an **annotation** (`@Timeout`) — more precise, allowing inspection of the thrown exception and clear scoping of which code is being checked.
- What is the practical difference between assertTimeout and assertTimeoutPreemptively?assertTimeout runs the code on the current thread to completion and only then reports failure if it overran — it cannot stop a hang. assertTimeoutPreemptively runs the code on a separate thread and aborts it the instant the limit passes, so it can catch an infinite loop, but the code runs on a different thread (watch out for ThreadLocal/transaction context).
- Why is assertThrows considered more precise than @Test(expected=...)?It scopes the expectation to a single lambda rather than the whole method, so the exception must come from exactly the code under test; and it returns the caught exception, letting you assert on its type, message, and cause.
saying these in an interview costs you the question
- Still writing @Test(expected=...) in JUnit 5 — it won't compile against Jupiter's @Test
- Thinking assertThrows checks the whole method — it only checks the lambda you pass
- Confusing assertTimeout (runs to completion) with assertTimeoutPreemptively (aborts)
- Forgetting assertThrows returns the exception, then not asserting on it when needed