skip to content

Which exception underlies a failed precondition check in JUnit 5's Assumptions class, and what happens if that check sits in a @BeforeEach or @BeforeAll method instead of the test body?

level: middleimportance: should knowfreq 27%

answer

  1. org.opentest4j.TestAbortedException
  2. type-driven mapping: AssertionError=failed, TestAbortedException=aborted
  3. @BeforeEach abort → that test only
  4. @BeforeAll abort → whole container skipped
  5. @AfterEach still runs; never assume in teardown

basics

~20 s

It is org.opentest4j.TestAbortedException, which Jupiter maps to aborted rather than failed. Thrown from @BeforeEach it aborts that single test; thrown from @BeforeAll it aborts the whole container, so every test in the class is reported as skipped.

solid answer

~50 s

`Assumptions.assumeTrue`/`assumeFalse` throw `org.opentest4j.TestAbortedException` — from the engine-neutral opentest4j library the JUnit Platform builds on. Jupiter treats that type specially: `AssertionError` means failed, `TestAbortedException` means **aborted**, anything else means failed. You can throw it directly yourself, and recent Jupiter versions expose `Assumptions.abort(...)` for an unconditional skip. Placement decides the blast radius: - In the **test body**, only that test aborts. - In **`@BeforeEach`**, the corresponding test never runs and is reported as aborted — repeated for each test in the class, which is the idiomatic way to gate a whole class on a runtime probe. `@AfterEach` callbacks matching already-executed `@BeforeEach` methods still run. - In **`@BeforeAll`**, the container itself is aborted: no test in the class executes and the whole class shows as skipped. - In a parameterized or repeated test, only the current invocation aborts; the siblings still run. Put assumptions before the exercised behaviour, never in teardown, where an abort cannot undo work already done.

code

java · 22 lines
java
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.opentest4j.TestAbortedException;

class BrokerIntegrationTest {

    @BeforeAll
    static void requireBroker() {
        // aborts the whole container: every test shows as skipped
        assumeTrue(portOpen("localhost", 9092), "Kafka not running on :9092");
    }

    @Test
    void publishes() {
        if (topicMissing("orders")) {
            throw new TestAbortedException("topic 'orders' not provisioned");
        }
        // ...
    }
}

go deeper

for a junior

Know the exception name and that an assumption in setup skips the test rather than failing it.

for a middle

Explain the type-driven status mapping and the difference in blast radius between @BeforeEach, @BeforeAll and the test body, including per-invocation behaviour for parameterized tests.

for a senior

Add the operational angle: cleanup guarantees, helpers that must rethrow TestAbortedException untouched, and the coverage risk of a whole class silently skipping.

for a principal

Compare runtime aborts with an ExecutionCondition extension for gating, and discuss reporting granularity and skip-drift monitoring as a suite-level concern.

## opentest4j and the status mapping JUnit 5 was designed so that assertion libraries and testing engines could interoperate. That is what `opentest4j` is: a tiny dependency defining a common vocabulary of throwables — `AssertionFailedError`, `MultipleFailuresError`, `TestAbortedException`, `TestSkippedException` — that any library can throw and any engine can interpret. Jupiter's outcome mapping is therefore type-driven: - nothing thrown → **successful**; - `TestAbortedException` → **aborted**; - `AssertionError` (including opentest4j's `AssertionFailedError`, and whatever AssertJ or Hamcrest throw) → **failed**; - any other throwable → **failed**, with the exception attached. `Assumptions` is a thin façade: it evaluates a boolean and throws `TestAbortedException` with an "assumption is not fulfilled" style message plus whatever message you supplied. Nothing stops you from throwing it directly, which is handy when the skip decision is buried inside a helper or a `switch` branch, and Jupiter also offers `Assumptions.abort()` / `abort(String)` for the unconditional case (added in a later 5.x release). One practical consequence of the type-driven mapping: if you catch `Throwable` broadly inside a test or a custom helper, you can swallow an abort and turn an honest skip into a pass. Similarly, code that wraps exceptions — a retry helper, a functional interface adapter — can convert `TestAbortedException` into a `RuntimeException` and thus a failure. When writing test utilities, rethrow it untouched. ## Aborting from lifecycle methods ### @BeforeEach An abort in `@BeforeEach` aborts the test that was about to run. Jupiter reports the individual test as aborted with the assumption's message, and it repeats for every test in the class, so the class ends up entirely skipped while remaining visible test-by-test in the report. This is the standard way to gate a whole class on something only knowable at runtime — probing a port, checking a daemon, reading a computed flag. Cleanup semantics are the useful part: Jupiter guarantees that `@AfterEach` callbacks run for the corresponding `@BeforeEach` methods that already completed. So if the first of two `@BeforeEach` methods started a container and the second aborted, the teardown still executes and the container is released. Extension callbacks follow the same wrapping discipline. ### @BeforeAll An abort in `@BeforeAll` targets the **container**, not a test. The class's tests are never started; the whole class is reported as skipped with the reason. This is cheaper than gating in `@BeforeEach` when the probe is expensive — you pay for it once — and it produces a cleaner report line for "this entire class did not apply here". The cost is granularity: you lose the per-test rows in the report. ### @AfterEach / @AfterAll Do not put assumptions in teardown. By then the behaviour has already been exercised and any verdict has been recorded; aborting afterwards cannot un-run the test and only produces a confusing outcome. Preconditions belong before the action, which is why the lifecycle hooks are the natural home. ### Templates and factories For `@ParameterizedTest` and `@RepeatedTest`, each invocation is its own test, so an assumption inside the method aborts only that invocation — a neat way to skip individual argument sets that cannot apply in the current environment while running the rest. For a `@TestFactory`, an assumption in the factory method aborts the container of dynamic tests; one inside an individual dynamic test's executable aborts only that dynamic test. ## Nested classes and extensions Aborting from an enclosing class's `@BeforeAll` skips nested classes as well, since they are contained within it. Extensions can achieve the same effect more idiomatically with an `ExecutionCondition`, which is evaluated before the test instance is even constructed and reports a disabled reason rather than an abort — cheaper and more visible when the decision does not require running fixture code. ## Reporting Aborted tests appear in the standard XML report as skipped elements, so CI dashboards, IDE runners and coverage tools all show them as skipped, typically with the message. Because the build stays green, teams that lean on lifecycle-level aborts should watch the skip count: a `@BeforeAll` abort silently removes an entire class of coverage, and that is exactly the kind of drift a green pipeline will never tell you about.

  • A @BeforeEach aborts. Does the matching @AfterEach still run?
    Yes, for the @BeforeEach methods that already completed. Jupiter's lifecycle guarantees that teardown pairs with setup that actually executed, so resources acquired before the abort are released. This is why probing and gating belong in setup rather than at the top of every test body — the framework's cleanup contract still holds.
  • How does an abort from @BeforeAll differ in the report from aborting inside every test?
    Aborting in @BeforeAll aborts the container, so the class appears as a single skipped entry and no individual tests are started or listed as executed. Aborting in @BeforeEach or in each test body produces one skipped entry per test, which is more granular but pays the probe cost per test. Choose @BeforeAll when the probe is expensive and the class is all-or-nothing, and per-test gating when you want visibility of each case.

saying these in an interview costs you the question

  • Naming AssertionError or a custom exception instead of org.opentest4j.TestAbortedException.
  • Believing an abort in @BeforeAll skips only the first test rather than the whole class.
  • Thinking an aborted setup prevents @AfterEach from running, and therefore duplicating cleanup inline.
  • Placing assumptions in @AfterEach, where the behaviour has already been exercised.
  • Writing test helpers that catch Throwable or wrap exceptions, converting an honest abort into a pass or a failure.

context