skip to content

Conditional Execution

Annotations that enable or disable tests per OS, JRE, system property, or custom logic. Interviewers contrast these declarative conditions with runtime assumptions.

on this pageshow

questions

5

JUnit 5 ships annotations that switch a test off depending on the environment it runs in — for example @EnabledOnOs and @DisabledOnJre. Name the main members of that family, describe how they can be applied, and explain what shows up in the report for a test they switch off.

level: juniorimportance: must knowfreq 48%

answer

  1. package org.junit.jupiter.api.condition
  2. OS / JRE / SystemProperty / EnvironmentVariable / EnabledIf
  3. class-level disables the whole container
  4. result = skipped + disabledReason
  5. backed by ExecutionCondition extensions

basics

~20 s

JUnit 5 has @EnabledOnOs/@DisabledOnOs, @EnabledOnJre/@DisabledOnJre and @EnabledForJreRange/@DisabledForJreRange, plus @EnabledIfSystemProperty/@EnabledIfEnvironmentVariable and method-based @EnabledIf/@DisabledIf. They go on a test method or a whole class, and a switched-off test is reported as skipped with a reason — it never runs.

solid answer

~40 s

The family lives in `org.junit.jupiter.api.condition`: - **OS/architecture:** `@EnabledOnOs(OS.LINUX)`, `@DisabledOnOs(OS.WINDOWS)`, with an `architectures` attribute for CPU architecture. - **Java version:** `@EnabledOnJre(JRE.JAVA_17)`, `@DisabledOnJre`, and range forms `@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)` / `@DisabledForJreRange`. - **Ambient config:** `@EnabledIfSystemProperty(named=..., matches=...)`, `@EnabledIfEnvironmentVariable(...)` and their `Disabled` twins — `matches` is a regex. - **Custom logic:** `@EnabledIf("methodName")` / `@DisabledIf(...)` naming a boolean method. All of them can be placed on a test method or on the class (where a disabled class skips the whole container, including `@BeforeAll`). They are implemented as `ExecutionCondition` extensions, evaluated before the test instance is created. A disabled test is reported as **skipped** with a reason — default text like "Disabled on OS: ...", or the `disabledReason` you supply — so it stays visible in the report instead of silently disappearing.

code

java · 22 lines
java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;

class FileSystemTest {

    @Test
    @EnabledOnOs(value = OS.LINUX, disabledReason = "uses inotify")
    void watchesDirectory() { }

    @Test
    @DisabledOnJre(JRE.JAVA_17)
    void usesNewerApi() { }

    @Test
    @EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)
    void supportedRangeOnly() { }
}

@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
class NightlyIntegrationTest {
    @Test void slowEndToEndFlow() { }   // whole class skipped locally
}

go deeper

for a junior

Name the annotations, show one on a method, and say the test is reported as skipped rather than passing.

for a middle

Add placement semantics (class-level skips the container including @BeforeAll), meta-annotation reuse, ANDed multiple conditions, and disabledReason.

for a senior

Discuss when environment-gating is the right call at all versus making the test environment-independent, and the reporting discipline of skipped-with-reason so gated tests do not rot unnoticed.

for a principal

Treat conditionally-disabled tests as a coverage risk to be tracked — a test permanently skipped in every environment is dead code that still looks like coverage; policy should require a named reason and a place where skips are reviewed.

## The problem they solve Some tests are genuinely environment-bound: a file-locking test that only makes sense on Windows, a test using an API added in a newer Java release, an integration test that needs credentials present only in CI. JUnit 5 lets you express that declaratively rather than with an `if` and a `return` that silently reports a green test that never ran. ## The catalogue All of these are in `org.junit.jupiter.api.condition`, and every one has an `Enabled...` and a `Disabled...` form: | Concern | Annotations | |---|---| | Operating system / CPU | `@EnabledOnOs`, `@DisabledOnOs` — take `OS` enum values (`LINUX`, `MAC`, `WINDOWS`, `AIX`, `SOLARIS`, `FREEBSD`, `OTHER`) and optionally `architectures` (for example `"aarch64"`) | | Java runtime version | `@EnabledOnJre`, `@DisabledOnJre` (specific versions), `@EnabledForJreRange`, `@DisabledForJreRange` (`min`/`max`) | | System property | `@EnabledIfSystemProperty`, `@DisabledIfSystemProperty` (`named` + `matches` regex) | | Environment variable | `@EnabledIfEnvironmentVariable`, `@DisabledIfEnvironmentVariable` (`named` + `matches` regex) | | Arbitrary logic | `@EnabledIf`, `@DisabledIf` — name a boolean method | There is also `@Disabled`, the unconditional one, which is not part of this family conceptually but shares the reporting behaviour. ## How they are applied - **On a method:** only that test is affected. - **On a class:** the entire container is skipped. Nothing inside runs — not the test methods, not `@BeforeAll`, not `@Nested` children. This matters: people sometimes expect class-level setup to still execute. - **On a custom annotation:** these are meta-annotatable, so you can define `@IntegrationTest` as `@Test @EnabledIfEnvironmentVariable(named = "CI", matches = "true")` and use one annotation everywhere. - **Combined:** several conditions on the same element are all evaluated, and **any** condition that says "disabled" wins — they are ANDed for enablement. The OS/JRE/property/env annotations are repeatable, so you can list several system-property requirements on one element. ## The `JRE` enum and ranges `@EnabledOnJre(JRE.JAVA_21)` matches only that feature release. `@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)` matches the inclusive range; omitting `min` means "from the lowest supported", omitting `max` means "up to the highest known". The catch is `JRE.OTHER`: when the tests run on a Java release newer than the enum constants known to your JUnit version, the detected JRE is `OTHER` and it falls outside every named range — so `@EnabledForJreRange(min = JRE.JAVA_17)` can unexpectedly *disable* a test on a brand-new JDK. Recent JUnit versions add integer `minVersion`/`maxVersion` attributes precisely to avoid depending on the enum being up to date. ## Reporting: skipped, with a reason When a condition disables an element, Jupiter reports it as **skipped** and attaches a reason. The defaults are descriptive ("Disabled on JRE version: 25", "@EnabledIfEnvironmentVariable(\"CI\") does not match ..."). Every annotation in the family also has a `disabledReason` attribute for a human explanation, which is what a reviewer reads six months later: ```java @DisabledOnOs(value = OS.WINDOWS, disabledReason = "POSIX file permissions unavailable") ``` This visibility is the argument for annotations over an early `return`: the test count shows the test was not executed, which an `if` hides. ## Conditions vs assumptions Both avoid running code in the wrong environment, but they differ in *when* and *what they report*. A condition is evaluated **before** the test instance is created and yields a **skipped** result; an assumption (`Assumptions.assumeTrue(...)`) runs **inside** the test after setup and yields an **aborted** result, which is why it is the right tool when the decision depends on state only available mid-test. Declarative environment facts — OS, Java version, presence of an env var — belong in conditions. ## Under the hood Each annotation is backed by an `ExecutionCondition` extension registered on the annotation itself. That is why the whole family behaves consistently, why custom conditions written as `ExecutionCondition` implementations plug in the same way, and why the platform offers a switch to deactivate conditions wholesale when you need to force a skipped test to run.

  • If you put @DisabledOnOs(OS.WINDOWS) on the test class, does @BeforeAll still run on Windows?
    No. A class-level condition disables the container itself, so Jupiter skips the whole class: no test instances, no @BeforeAll or @AfterAll, and no nested classes. The container is reported as skipped with the reason. If you need class setup to run regardless, the condition belongs on the individual methods instead.
  • When would you use Assumptions.assumeTrue instead of one of these annotations?
    When the decision depends on state that only exists once the test is running — a service responded, a fixture returned no data, a feature flag fetched at runtime. Conditions are evaluated before the test instance is created, so they can only see ambient facts such as the OS, Java version, system properties and environment variables. Assumptions also report the test as aborted rather than skipped.
  • How do you avoid repeating the same condition on twenty test classes?
    Meta-annotate: define your own annotation, for example @IntegrationTest, and put @Test plus the condition annotations on it. Jupiter resolves conditions through meta-annotations, so the single custom annotation carries the behaviour and gives the rule a name that reads well in the code.

saying these in an interview costs you the question

  • Thinking a disabled test is reported as passing, or disappears from the report entirely
  • Expecting @BeforeAll to still run when the condition is on the class
  • Believing these annotations only work on methods
  • Confusing conditions (evaluated before instance creation, reported skipped) with assumptions (evaluated inside the test, reported aborted)
  • Assuming two conditions on one element are ORed so any single satisfied condition enables the test

context

open as a page

JUnit 5's @EnabledIf and @DisabledIf take the name of a condition method rather than a value. What are the rules for that method — its signature, visibility, where it may live, and when it is invoked — and when would you reach for it instead of the OS/JRE/property annotations?

level: middleimportance: should knowfreq 30%

basics

~20 s

The named method must return boolean, take no arguments, and not be private. It must be static unless the class uses @TestInstance(PER_CLASS), because conditions are evaluated before the test instance exists. It can live in the test class or be referenced by fully qualified name like com.acme.Conditions#onCi, and it is evaluated per annotated element.

open as a page

A JUnit 5 test method carries @EnabledIfSystemProperty(named = "env", matches = "ci"). Under exactly which values of that system property does the test run, what happens when the property is not set at all, and how does @EnabledIfEnvironmentVariable differ in practice?

level: middleimportance: should knowfreq 36%

basics

~20 s

matches is a regular expression that must match the property's ENTIRE value, so only the exact value "ci" enables it — "ci-eu" does not. If the property is absent the test is disabled, because a missing value cannot match. @EnabledIfEnvironmentVariable is identical but reads the process environment, which the JVM cannot set at runtime.

open as a page

Explain the difference between JUnit 5's @EnabledOnJre and @EnabledForJreRange, and describe what happens to a test annotated @EnabledForJreRange(min = JRE.JAVA_17) when it runs on a Java release newer than any constant your JUnit version knows about.

level: middleimportance: nice to knowfreq 22%

basics

~20 s

@EnabledOnJre lists exact feature releases; @EnabledForJreRange gives an inclusive min/max span. On a Java release newer than the JRE enum knows, JUnit reports JRE.OTHER, which falls outside every named range — so the test is unexpectedly skipped. Newer JUnit versions add integer minVersion/maxVersion to avoid this.

open as a page

A JUnit 5 test is being skipped in CI because of an environment-based condition annotation, and you need to force it to execute once to reproduce a failure without editing the source or the annotation. What mechanism does JUnit 5 provide for that, and what are the risks of using it?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Set the JUnit configuration parameter junit.jupiter.conditions.deactivate to a pattern matching the ExecutionCondition classes to switch off — for example org.junit.* or *. Matching conditions stop being consulted, so gated tests run. The risk: it is coarse, and a broad pattern also deactivates @Disabled, resurrecting quarantined tests.

open as a page