JUnit 5's Assertions class offers both assertTimeout and assertTimeoutPreemptively. What is the difference in how each one executes the code under test, and when would you choose one over the other?
answer
- assertTimeout = stopwatch, same thread, fails after
- preemptively = executor thread + deadline, aborts
- preemptive kills only via interrupt → leaks
- ThreadLocal/transaction/security context lost
- default to non-preemptive
basics
~20 sassertTimeout runs the block on the test thread, lets it finish, then fails if it overran. assertTimeoutPreemptively runs it on a separate thread and aborts at the deadline, so it can catch hangs — but the code loses the test thread's thread-local state.
solid answer
~60 sBoth take a `Duration` and a block, but the execution model differs. `assertTimeout` executes the block **on the current test thread** and waits for it to return. Timing is checked afterwards, so a slow block still runs to completion and a hung block hangs the test forever. Nothing thread-bound is disturbed. `assertTimeoutPreemptively` submits the block to a **separate thread** via an executor and waits with a deadline. If the deadline passes it fails immediately with `execution timed out after 500 ms` and cancels the task, interrupting that thread. It is the only one of the two that gives real hang protection and that bounds the wall-clock cost of a runaway test. The price is thread affinity. Anything held in a `ThreadLocal` on the test thread — a Spring transaction and its bound `EntityManager`, `SecurityContextHolder`, logging MDC — is invisible to the worker thread, so preemptive timeouts routinely break transactional or security-aware tests. Also, cancellation is only an interrupt: uninterruptible work keeps running in the background. Default to `assertTimeout`; reach for the preemptive form only for genuinely hang-prone, context-free code.
code
java · 13 lines@Test
@Transactional
void nonPreemptive_keepsTransactionAndSession() {
Order o = assertTimeout(Duration.ofMillis(500),
() -> orderService.place(cart)); // same thread: tx + EntityManager bound
assertEquals(2, o.getLines().size()); // lazy load still works
}
@Test
void preemptive_forHangPronePureCode() {
assertTimeoutPreemptively(Duration.ofSeconds(1),
() -> parser.parse(pathologicalInput)); // runs on an executor thread
}go deeper
State the core contrast: one runs on the test thread and fails afterwards, the other runs on another thread and aborts at the deadline.
Add the consequences — hang protection versus lost thread-local state — and say you default to the non-preemptive form.
Bring in concrete breakage (transaction rollback, lazy loading, SecurityContextHolder, MDC), interrupt-only cancellation and thread leaks, and argue for fixing the underlying missing timeout instead.
Position per-assertion timeouts against suite policy: declarative @Timeout or global timeout configuration for hang protection, real client timeouts in production code, and assertions reserved for narrow regression guards.
## The two methods `org.junit.jupiter.api.Assertions` provides: ```java assertTimeout(Duration timeout, Executable executable) assertTimeoutPreemptively(Duration timeout, Executable executable) ``` plus `ThrowingSupplier` overloads that return the block's value, and optional message parameters. The signatures look interchangeable; the runtime behaviour is not. ## assertTimeout — same thread, runs to completion The non-preemptive form is essentially "stopwatch around a call". JUnit records a start time, invokes the block **on the thread already running the test**, waits for it to return, and only then compares elapsed time against the budget. The failure message reports the overrun: `execution exceeded timeout of 500 ms by 132 ms`. Properties that follow: - **No cancellation.** A block budgeted at 100 ms that takes 30 s runs for the full 30 s; the suite pays that cost and then reports the failure. - **No hang protection.** A deadlock or an infinite loop inside the block hangs the test indefinitely. Only a build-level or `@Timeout`-level mechanism can rescue you. - **Full context fidelity.** The block sees exactly the state the test method sees: the same transaction, the same `ThreadLocal`s, the same interrupt status, the same stack. Nothing is lost. ## assertTimeoutPreemptively — separate thread, aborts at the deadline The preemptive form submits the block to an executor thread and waits on the resulting future with the given deadline. Two outcomes: - The block completes in time → its result is returned (supplier form) and any throwable it threw is rethrown. - The deadline elapses first → JUnit throws an `AssertionFailedError` reading `execution timed out after 500 ms`, and cancels the future, which **interrupts** the worker thread. Properties that follow: - **Real hang protection.** The test fails at the deadline instead of blocking the suite. This is the entire reason the method exists. - **Bounded cost.** A runaway test costs the budget, not its full runtime. - **Interrupt-only cancellation.** `Future.cancel(true)` sets the interrupt flag. Code that ignores interrupts — a tight CPU loop, a blocking native call, a socket read without an SO_TIMEOUT — keeps running after the assertion has already failed. The thread leaks, and it may still hold locks, keep a connection checked out, or write to a resource the next test is using. - **Lost thread-bound state.** This is the trap that bites teams in practice, and it deserves its own section. ## The thread-affinity problem Enormous amounts of Java infrastructure store state in `ThreadLocal`s: - Spring's `TransactionSynchronizationManager` binds the transaction and the JPA `EntityManager`/Hibernate `Session` to the current thread. A `@Transactional` test's rollback semantics and its persistence context live there. - Spring Security's default `SecurityContextHolder` strategy is `ThreadLocal`. - SLF4J/Logback MDC, request-scoped beans, `RequestContextHolder`, many tracing agents' spans. Code running under `assertTimeoutPreemptively` executes on a **different thread**, so none of that is visible. Typical symptoms: `LazyInitializationException` because there is no bound session; the work committing to the real database because it ran outside the test's rolled-back transaction; `NullPointerException` or `AccessDeniedException` because the authentication is absent; log lines missing correlation ids. The failure often looks nothing like a timeout problem, which makes it expensive to diagnose. JUnit's own documentation warns about exactly this and recommends the non-preemptive variant when the code relies on thread-bound state. ## Choosing between them A workable rule: - **Default to `assertTimeout`.** It is the honest "this got slower" guard, and it never corrupts context. - **Use `assertTimeoutPreemptively` only when** the code under test is self-contained (no transaction, no security context, no MDC), is genuinely capable of hanging, and responds to interrupts. - **Prefer fixing the root cause** where possible: give the client a real read/connect timeout, give the lock acquisition a `tryLock(timeout)`, give the queue poll a deadline. A test-side preemptive timeout papers over a production defect — the same hang will occur in production, where no JUnit assertion is watching. - **For suite-wide hang protection**, prefer the declarative `@Timeout` annotation (and its separate-thread mode, available since Jupiter 5.9) or a global timeout configuration parameter, rather than sprinkling preemptive assertions through test bodies. Those are policy; assertions are per-case statements about the code. ## Summary table in words Same thread, completes, fails after, safe context, no hang protection → `assertTimeout`. Separate thread, aborts at deadline, hang protection, interrupt-only cancellation, thread-local state lost → `assertTimeoutPreemptively`.
- assertTimeoutPreemptively fails a test at the deadline. Is the code it was running guaranteed to have stopped?No. JUnit cancels the task, which interrupts the worker thread; it cannot force-stop it. Code that never checks the interrupt flag or blocks in an uninterruptible call keeps running after the assertion has already failed, so it can leak a thread, hold a lock or a pooled connection, and interfere with subsequent tests.
- Your @Transactional Spring test starts failing with LazyInitializationException after someone wraps the call in assertTimeoutPreemptively. Why?The transaction and the bound Hibernate Session live in a ThreadLocal on the test thread, and the preemptive assertion executes the block on a different executor thread that has no such binding. The entity is therefore detached there, so touching a lazy association throws. Switching to plain assertTimeout, which runs on the test thread, restores the binding.
assertTimeout is a stopwatch on the finish line — the runner always finishes, you just record they were late. assertTimeoutPreemptively is a marshal who pulls the runner off the course at the cut-off — faster to know, but the runner is now off the course carrying your baton.
saying these in an interview costs you the question
- Treating the two methods as interchangeable because the signatures match
- Claiming preemptive cancellation reliably stops the running code (it is only an interrupt)
- Not knowing that thread-bound transaction/security/MDC state is lost on the separate thread
- Reaching for preemptive timeouts to hide a missing client-side socket or lock timeout