skip to content

In JUnit 5, how do you make a test stop at runtime and report itself as skipped when a precondition is not satisfied, and how does that outcome differ from a failure?

level: juniorimportance: must knowfreq 45%

answer

  1. Assumptions.assumeTrue / assumeFalse
  2. throws opentest4j TestAbortedException
  3. status = aborted, reported as skipped, build stays green
  4. assertion = code is wrong; assumption = could not check here
  5. always pass a reason message

basics

~20 s

Call Assumptions.assumeTrue(condition) (or assumeFalse) at the top of the test. If the condition does not hold, a TestAbortedException is thrown, the rest of the method never runs, and the test is reported as aborted or skipped — the build stays green rather than red.

solid answer

~50 s

JUnit Jupiter's `org.junit.jupiter.api.Assumptions` provides `assumeTrue`/`assumeFalse`, each taking a boolean or a `BooleanSupplier`, optionally with a message or message supplier. An assumption states a precondition for the test to be *meaningful*. If it does not hold, Jupiter throws `org.opentest4j.TestAbortedException`; execution of the method stops there and the test is recorded with the **aborted** status, which build tools and IDEs surface as skipped. The build stays green. That is the key contrast with an assertion. `assertTrue(x)` failing means "the code is wrong" — red build, someone must act. `assumeTrue(x)` failing means "we could not run this check here" — no verdict on the code at all. Typical use is environment gating: skip when a required service is unreachable, when running on the wrong OS, or when a credential is absent, instead of failing a build for reasons unrelated to the change under test.

code

java · 15 lines
java
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class PaymentGatewayTest {

    @Test
    void chargesSandboxAccount() {
        assumeTrue(System.getenv("GATEWAY_KEY") != null,
                   "GATEWAY_KEY not set; skipping sandbox test");

        assertEquals("APPROVED", gateway.charge(1000).status());
    }
}

go deeper

for a junior

Name Assumptions.assumeTrue, say the test is skipped rather than failed, and give one environment-gating example.

for a middle

Add the underlying TestAbortedException, the aborted status and how build reports display it, and the BooleanSupplier and message overloads.

for a senior

Emphasise the judgement: assumptions gate the environment, never the result; overuse produces green builds with silent coverage loss that must be monitored.

for a principal

Discuss the organisational effect — skipped-test drift, the choice between runtime gating and running the required environment properly, and treating skip counts as a tracked signal.

## Three outcomes, not two Most people think of tests as passing or failing. The JUnit Platform actually records four statuses at the test level: successful, failed, aborted, and (for tests never started) skipped. **Aborted** is the one assumptions produce: the test began, decided its preconditions were not met, and stopped without rendering a verdict on the code. The distinction matters because failure is a call to action. A red build says "a change broke something; find it". A test that fails because Docker is not installed on a laptop teaches developers to ignore red, which is the most expensive thing you can do to a test suite. Aborting instead says "nothing was checked here", leaving the build green while being honest that coverage was not delivered. ## The API `org.junit.jupiter.api.Assumptions` is a static-method utility, usually imported statically: - `assumeTrue(boolean)` / `assumeTrue(BooleanSupplier)` — abort unless the condition holds. - `assumeFalse(boolean)` / `assumeFalse(BooleanSupplier)` — the inverse. - Both accept an extra `String` message or `Supplier<String>` explaining the skip; the message ends up in the report, and supplying one is close to mandatory in practice — a skip with no reason is undiagnosable months later. - `assumingThat(condition, Executable)` runs a block only when the condition holds, without aborting anything. - Recent Jupiter versions also expose `abort()` / `abort(String)` to skip unconditionally from inside a branch. The `BooleanSupplier` overloads exist so an expensive or throwing check is only evaluated when needed and reads cleanly as a lambda. ## Mechanics A failed assumption is not magic: `Assumptions` throws `org.opentest4j.TestAbortedException`. Opentest4j is the small, engine-neutral exception library the JUnit Platform uses so different testing frameworks and assertion libraries can agree on what a failure and an abort look like. Jupiter catches that exception type and maps it to the aborted status, whereas an `AssertionError` (what `Assertions.fail` and assertion libraries throw) maps to failed. Because it is an exception, control flow stops immediately at the assumption. Everything after it in the method — including any cleanup written inline — does not run. Teardown declared with `@AfterEach` still runs normally, since Jupiter guarantees callbacks for lifecycle phases that already executed. You can also throw `TestAbortedException` yourself; `Assumptions` is just readable sugar over that. ## Where the outcome shows up Standard XML test reports carry a skipped element for aborted tests, so CI dashboards, IDEs, and coverage tooling all display them as skipped, generally with the assumption message as the reason. This is exactly what makes assumptions dangerous when overused: a suite where half the tests abort still looks green, and nobody notices that a misspelled environment variable turned off an entire integration layer. ## Typical use cases - **Environment gating.** `assumeTrue(dockerAvailable(), "Docker daemon not reachable")` before a container-backed test. - **Platform gating.** Skipping a filesystem-permissions test on an OS whose semantics differ. - **Credential gating.** Skipping tests against a third-party sandbox when the API key is not configured, so external contributors' builds still pass. - **Data gating.** Skipping when an optional fixture dataset is absent. ## When not to use one If the condition is knowable *before* the test runs and does not depend on computed state, a declarative execution condition is usually better: it is visible without reading the body and avoids constructing the test instance and its fixtures at all. Assumptions earn their place when the check requires work — opening a socket, probing a service, inspecting a fixture — that only makes sense at runtime. And never use an assumption to paper over a flaky or failing test. `assumeTrue(!isCi())` on a test that fails in CI is not gating, it is hiding a bug behind a green build. The rule of thumb: an assumption should describe the *environment*, never the *result*. ## JUnit 4 comparison JUnit 4 had the same idea in `org.junit.Assume`, throwing `AssumptionViolatedException`. Jupiter kept the concept and moved to the opentest4j exception. The reporting semantics — skipped rather than failed — are the same.

  • What happens to the rest of the test method, and to @AfterEach, when an assumption fails?
    The assumption throws TestAbortedException, so the remainder of the method body never executes — nothing after that line runs, including inline cleanup. Lifecycle callbacks behave normally: any @AfterEach that corresponds to a @BeforeEach which already ran is still invoked, so resources acquired in setup are released. That is why cleanup belongs in lifecycle methods rather than at the end of the body.
  • Why is it wrong to use an assumption to silence a test that fails only in CI?
    Because an assumption asserts something about the environment, not about the code, and skipping produces a green build. Gating on an environment flag to dodge a real failure hides a genuine defect while removing the coverage that would catch it, and the skip is invisible in a green pipeline. The honest options are to fix the underlying issue, or to mark the test as disabled with a tracked reason so its absence is visible.

An assertion is a referee calling a foul; an assumption is the referee calling off the match because the pitch is waterlogged — no result is recorded either way.

saying these in an interview costs you the question

  • Saying a failed assumption fails the test — it aborts it, and the build stays green.
  • Confusing assumeTrue with assertTrue and using assumptions to check business logic.
  • Using an assumption to hide a genuinely failing or flaky test.
  • Omitting the message, leaving skips that nobody can diagnose later.
  • Assuming code after the failed assumption still runs, so writing cleanup inline instead of in @AfterEach.

context