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.
answer
- package org.junit.jupiter.api.condition
- OS / JRE / SystemProperty / EnvironmentVariable / EnabledIf
- class-level disables the whole container
- result = skipped + disabledReason
- backed by ExecutionCondition extensions
basics
~20 sJUnit 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 sThe 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 linesimport 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
Name the annotations, show one on a method, and say the test is reported as skipped rather than passing.
Add placement semantics (class-level skips the container including @BeforeAll), meta-annotation reuse, ANDed multiple conditions, and disabledReason.
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.
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