In JUnit 5 (Jupiter), how do you assert inside a test that a block of code finishes within a given time budget, and what does the framework report when the block is too slow?
answer
- Assertions.assertTimeout(Duration, Executable)
- ThrowingSupplier overload returns the value
- same thread, waits for completion
- fails AFTER the fact — no hang protection
- "exceeded timeout of 500 ms by 132 ms"
basics
~20 sUse Assertions.assertTimeout(Duration.ofMillis(500), () -> code). It runs the block on the test thread, then fails if it took longer, reporting the expected budget and how much it overran. A supplier form returns the block's value.
solid answer
~50 sJUnit Jupiter's `Assertions` class exposes `assertTimeout(Duration timeout, Executable executable)` and an overload taking a `ThrowingSupplier<T>` that returns the block's result, so you can keep asserting on it afterwards. The budget is expressed as a `java.time.Duration` — `Duration.ofMillis(500)`, `Duration.ofSeconds(2)` — never a raw long, which removes unit ambiguity. The key mechanic: `assertTimeout` runs the block **on the calling test thread and waits for it to complete**. Only after it returns does JUnit compare elapsed time against the budget and fail with a message like `execution exceeded timeout of 500 ms by 132 ms`. It therefore cannot interrupt a hang — if the code blocks forever, the test blocks forever. If the block throws, that exception is propagated (checked exceptions are allowed because `Executable`/`ThrowingSupplier` declare `throws Throwable`), so a failing block fails the test on its own error, not on time. Optional third argument adds a message or `Supplier<String>` message.
go deeper
Recall the signature, the Duration argument, and that the failure happens after the block finishes rather than interrupting it.
Add the two overloads (Executable vs ThrowingSupplier), the exact failure message shape, and why exceptions from the block win over timing.
Frame it as the safe same-thread variant that preserves thread-bound context, and note it gives zero hang protection so it is not a watchdog.
Discuss where wall-clock assertions belong at all: gross-regression guards in tests, real latency objectives in load tests and production SLOs, and budget sizing against CI noise.
## What a timeout assertion is A timeout assertion answers the question "did this piece of code finish fast enough?" from inside a test method. JUnit 5's assertion entry point is the class `org.junit.jupiter.api.Assertions`, and the two relevant static methods are `assertTimeout` and `assertTimeoutPreemptively`. This answer covers the plain, non-preemptive one. ## The API shape There are two families of overloads: - `assertTimeout(Duration timeout, Executable executable)` — `Executable` is JUnit's functional interface with a single `void execute() throws Throwable` method. Use it when the block returns nothing. - `assertTimeout(Duration timeout, ThrowingSupplier<T> supplier)` — returns `T`, the value the block produced. This lets you write `Order order = assertTimeout(ofSeconds(2), () -> service.place(cart));` and then continue asserting on `order`. Both accept an optional third parameter: a `String` message or a `Supplier<String>` for a lazily built message, appended to the failure output. Because both functional interfaces declare `throws Throwable`, the lambda may throw checked exceptions without a try/catch — a small ergonomic win over writing the timing code by hand. ## Why Duration and not milliseconds The timeout is a `java.time.Duration`, constructed with `Duration.ofMillis(...)`, `Duration.ofSeconds(...)`, `Duration.ofMinutes(...)`. Passing a `Duration` makes the unit part of the value, so a reader never has to guess whether `500` meant milliseconds or seconds, and the same object can be shared as a constant across tests. Static-importing `java.time.Duration.ofMillis` keeps call sites short. ## Execution semantics — same thread, runs to completion This is the single most important behavioural fact. `assertTimeout` does **not** run the block in the background and does **not** stop it at the deadline. It records a start timestamp, invokes the block **on the current test thread**, waits for it to return, records the end timestamp, and only then compares elapsed time to the budget. Consequences: - A block that takes 10 seconds against a 1-second budget still runs for the full 10 seconds; the test simply fails afterwards. The suite pays the full wall-clock cost. - A block that never returns — a deadlock, an infinite loop, a socket read with no read timeout — hangs the test forever. `assertTimeout` gives you no protection against hangs. Hang protection needs `assertTimeoutPreemptively` or the `@Timeout` annotation with a separate-thread mode. - Everything bound to the test thread stays valid: an open transaction, a `ThreadLocal`-held security context, an MDC logging context, a bound persistence context. Nothing is copied or lost, which is exactly why the same-thread variant is the safe default in Spring or Jakarta EE style tests. ## Failure output On overrun, JUnit throws an `AssertionFailedError` whose message reads like `execution exceeded timeout of 500 ms by 132 ms`, optionally prefixed by your custom message. The "by" part is genuinely useful: an overrun of 2 ms reads very differently from an overrun of 4 seconds when you are deciding whether the budget or the code is wrong. If the block throws instead of finishing, the throwable is rethrown as-is (wrapped only where the language requires it), so you see the real stack trace rather than a timing failure. Timing is only evaluated on a normal return. ## Practical guidance Use `assertTimeout` to catch gross regressions — an operation that should be milliseconds suddenly doing network I/O or an N+1 query — not to assert precise latency. Wall-clock time on a shared CI machine is noisy: JIT warm-up, GC pauses, and neighbouring containers all move the number. Pick a budget several times larger than the observed steady-state cost so the test fails only on a real order-of-magnitude change. Also remember the assertion measures whatever is inside the lambda, including any setup you accidentally left there. Move fixture construction outside the block so you are timing the operation under test, not the arrangement. ## Minimal example ```java String body = assertTimeout(Duration.ofMillis(300), () -> renderer.render(template, model), "template rendering regressed"); assertTrue(body.contains("<h1>")); ``` That single call both bounds the work and hands you the result for further assertions, which is why the `ThrowingSupplier` overload is usually the one you want.
- If the code inside assertTimeout throws an exception instead of being slow, what does the test report?The throwable is propagated rather than converted into a timing failure, so the test fails with the original exception and stack trace. Both `Executable` and `ThrowingSupplier` declare `throws Throwable`, so checked exceptions need no try/catch in the lambda. Timing is only evaluated when the block returns normally.
- What happens if the block inside assertTimeout never returns at all?The test thread blocks indefinitely and the test never finishes, because the assertion only compares timestamps after the block completes. Protecting against hangs requires `assertTimeoutPreemptively`, or the `@Timeout` annotation configured to run in a separate thread, since those execute the work elsewhere and abandon it at the deadline.
saying these in an interview costs you the question
- Believing assertTimeout stops or cancels the code at the deadline
- Thinking a hung block will be killed and the test will still finish
- Passing a raw millisecond long instead of a Duration (no such overload in Jupiter)
- Using assertTimeout as a precise latency benchmark on shared CI hardware