In JUnit 5, how do you make a single test fail automatically if it runs longer than a chosen duration, and what exactly happens when that limit is reached?
answer
- org.junit.jupiter.api.Timeout
- default unit = SECONDS
- fails with TimeoutException
- works on lifecycle methods and classes too
- liveness guard, not a perf assertion
basics
~20 sAnnotate the test with JUnit Jupiter's @Timeout, e.g. @Timeout(value = 500, unit = TimeUnit.MILLISECONDS). The default unit is seconds. When the limit elapses the test fails with a TimeoutException naming the method and the configured duration.
solid answer
~40 sJUnit Jupiter provides `@Timeout` (org.junit.jupiter.api.Timeout). `@Timeout(5)` means five seconds: `value` is a long and `unit` is a `java.util.concurrent.TimeUnit` that defaults to `SECONDS`, so sub-second limits are written `@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)`. It is not restricted to `@Test`. It can annotate any testable method (`@Test`, `@ParameterizedTest`, `@RepeatedTest`, `@TestFactory`, `@TestTemplate`), any lifecycle method (`@BeforeAll`, `@BeforeEach`, `@AfterEach`, `@AfterAll`), or a whole test class. When the duration is exceeded, Jupiter reports the method as failed with a `java.util.concurrent.TimeoutException` whose message reads like `myTest() timed out after 500 milliseconds`. Whether the running code actually stops at that instant depends on the thread mode: by default the executing thread is interrupted, so code that ignores interruption keeps running and the failure only surfaces once the method finally returns.
code
java · 19 linesimport java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
class OrderServiceTest {
@BeforeEach
@Timeout(10) // 10 SECONDS
void startContainer() {
fixture.start();
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void priceLookupIsFast() {
service.price("SKU-1");
}
}go deeper
Know the annotation name, that the default unit is seconds, that you set unit explicitly for millisecond budgets, and that exceeding it fails the test.
Add where it can be placed (lifecycle methods, class level), the precedence over suite-wide defaults, and that the failure is a TimeoutException rather than an AssertionError.
Lead with the interrupt-versus-abandon distinction, why tight limits are a flakiness source on shared CI, and that a firing timeout is a bug signal to investigate rather than a number to raise.
Frame it as suite-level liveness policy: a generous global net so no job ever hangs, targeted limits only where a hang is plausible, and a separate mechanism (job-level watchdog) for code that ignores interruption.
## The problem it solves A test that never returns is worse than a failing test: it blocks the build, occupies a CI agent, and produces no report. `@Timeout` gives a test a wall-clock budget so a hang becomes an ordinary, reportable failure instead of a stuck job. ## The annotation `org.junit.jupiter.api.Timeout` has three attributes: - `value` — a `long` count of time units. - `unit` — a `java.util.concurrent.TimeUnit`, defaulting to `TimeUnit.SECONDS`. This default is the single most common surprise: `@Timeout(100)` is a hundred *seconds*, not a hundred milliseconds. - `threadMode` — controls whether the annotated method runs on the calling thread or a separate one. Its default is `INFERRED`, which resolves to the configured default (same-thread unless changed). ## Where it can be placed Jupiter applies `@Timeout` to: - testable methods: `@Test`, `@ParameterizedTest`, `@RepeatedTest`, `@TestFactory`, `@TestTemplate`; - lifecycle methods: `@BeforeAll`, `@BeforeEach`, `@AfterEach`, `@AfterAll` — useful because a hanging fixture (a container that never starts, a connection pool that never fills) is a very common cause of a stuck suite; - a test class, where it governs every testable and lifecycle method in that class and in its `@Nested` classes. For a `@TestFactory`, the budget covers factory-method execution, not the execution of every dynamic test it produces. ## What happens at the deadline The method is measured against wall-clock time, from the moment Jupiter invokes it. If it has not returned when the budget expires, the outcome is a failure carrying `java.util.concurrent.TimeoutException` with a message such as `slowQuery() timed out after 2 seconds`. Reporting-wise this is an ordinary failed test: it shows up red in the XML report and in the IDE, it does not abort the rest of the class, and subsequent tests keep running. What it is *not* is a kill switch. In the default same-thread mode, a watchdog thread interrupts the thread executing the test when the budget expires. Interruption only helps code that responds to it — `Thread.sleep`, `Object.wait`, `BlockingQueue.take`, `Future.get`, NIO interruptible channels. A tight CPU loop, a blocking `InputStream.read` on a classic socket, or a native call will keep going; the test is then still marked as timed out, but only once the method eventually returns, so the build was not actually rescued. Preemptive abandonment requires the separate-thread mode, which has its own costs. ## Precedence and defaults A method-level `@Timeout` beats a class-level one; a class-level one beats any suite-wide default configured through JUnit Platform configuration parameters. So the usual layering is: a generous suite-wide default acting as a deadlock net, overridden by the occasional targeted annotation where a hang is genuinely likely. ## How to use it well Treat `@Timeout` as a liveness guard, not a performance assertion. CI machines are shared, noisy, and often slower than a developer laptop; a limit set just above the local runtime turns into a flaky test. Choose a budget an order of magnitude above the expected duration, so it only fires when something is actually wrong. If you truly need to assert latency, that is a benchmark, not a unit test. Second, prefer fixing the hang over papering over it. A timeout that fires regularly is a bug report — a missing mock, an unbounded retry, a lock never released. Third, remember debugging: stepping through a test in a debugger easily blows a five-second budget, which is why the platform offers a mode that disables timeouts when a debugger is attached. ## Contrast with JUnit 4 JUnit 4 offered `@Test(timeout = 1000)` and the `Timeout` rule. `@Test(timeout=...)` always ran the test in a separate thread, which silently broke anything thread-affine (Spring's transactional test support, `ThreadLocal`-based context). Jupiter deliberately reversed that default: same thread by default, separate thread only when you ask.
- If a test blocks forever inside a call that ignores thread interruption, will @Timeout end the build's wait?Not in the default same-thread mode. Jupiter runs the method on the calling thread and a watchdog interrupts that thread at the deadline; if the code never checks the interrupt flag and is not sitting in an interruptible operation, it keeps running and the timeout failure is only recorded when the method eventually returns. To abandon the work preemptively you must set threadMode to SEPARATE_THREAD, which runs the body on another thread and lets the test fail immediately — at the price of leaking that thread and losing thread-affine state.
- Can @Timeout be applied to @BeforeAll, and what does it cover there?Yes. Jupiter supports @Timeout on all four lifecycle annotations as well as on testable methods. On @BeforeAll it budgets the one-time class setup, which is exactly where suites tend to hang — starting a container, waiting for a port, filling a pool. If the setup exceeds the budget it fails with a TimeoutException and the tests in that class are reported as not executed, rather than the job hanging.
It is a kitchen timer next to the stove, not a fire suppression system: it reliably tells you the dish has been in too long, but it does not necessarily turn the burner off.
saying these in an interview costs you the question
- Assuming the default unit is milliseconds, so @Timeout(100) is read as a tenth of a second instead of 100 seconds.
- Claiming @Timeout always kills the running test immediately — in the default same-thread mode it only interrupts, and uninterruptible code runs on.
- Thinking it only works on @Test methods and cannot guard setup or a whole class.
- Setting the limit just above the observed local runtime and turning the test flaky on a loaded CI machine.
- Using @Timeout as a performance assertion instead of a liveness guard.