skip to content

Walk through JUnit 5's conditional execution annotations - @EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable, @EnabledIf - and how each decides whether a test runs.

level: middleimportance: should knowfreq 48%

answer

  1. Package org.junit.jupiter.api.condition
  2. OS / JRE / system property / env var / @EnabledIf
  3. named + matches(regex) for property & env var
  4. @EnabledIf points at a boolean method
  5. Absent property/var => not matched => @Enabled disables

basics

~20 s

These annotations turn a test on or off based on a condition: the operating system (@EnabledOnOs), a JVM system property (@EnabledIfSystemProperty), an OS environment variable (@EnabledIfEnvironmentVariable), or a custom boolean method (@EnabledIf). Each has a @Disabled... twin that does the opposite.

solid answer

~40 s

JUnit 5's conditional annotations (in org.junit.jupiter.api.condition) gate a test on a runtime fact. @EnabledOnOs / @DisabledOnOs take OS enum values (WINDOWS, MAC, LINUX...). @EnabledOnJre / @DisabledOnJre and @EnabledForJreRange gate on the Java version. @EnabledIfSystemProperty(named="...", matches="regex") checks a JVM system property against a regex; if the property is absent the @Enabled form disables the test. @EnabledIfEnvironmentVariable does the same for OS environment variables. @EnabledIf / @DisabledIf are the general form: you point them at a method (or static method) returning boolean, and that decides. Each annotation contributes a human-readable reason to the skip report. You can put them on methods or whole classes, and combine them - if any condition disables the test, it is skipped. Conditions re-evaluate every run, so tests self-enable when the environment changes.

code

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

class EnvAwareTest {

    @Test
    @EnabledOnOs(OS.LINUX)
    void linuxOnly() { }

    @Test
    @EnabledIfEnvironmentVariable(named = "INTEGRATION_TESTS", matches = "true")
    void integrationOnly() { }

    @Test
    @EnabledIf("dbReachable")
    void needsDatabase() { }

    boolean dbReachable() {
        return Boolean.getBoolean("db.up"); // custom predicate
    }
}

go deeper

for a junior

Can name a couple of conditional annotations and say they enable/disable based on a condition.

for a middle

Knows the four built-in dimensions plus @EnabledIf, the named+matches(regex) shape, and that each has an Enabled/Disabled twin.

for a senior

Handles the absent-property semantics, system-property vs env-var distinction, and chooses the narrowest correct annotation; uses @EnabledIf with a shared static predicate.

for a principal

Standardizes how the org expresses test prerequisites declaratively, and designs custom ExecutionConditions/annotations when the built-ins don't fit.

## The idea: conditional test execution Sometimes a test only makes sense in a particular environment. JUnit 5 provides **conditional execution annotations** that decide, *at runtime*, whether a given test should run. They all live in the package `org.junit.jupiter.api.condition`, and each comes as an `@Enabled...` / `@Disabled...` pair (the `Enabled` form runs the test only when the condition holds; the `Disabled` form skips the test when the condition holds). They can annotate a single test **method** or an entire test **class** (applying to all its tests). Multiple conditions can be stacked; if *any* of them says 'disabled', the test is skipped. ## @EnabledOnOs / @DisabledOnOs Keyed on the **operating system** via the `OS` enum: `OS.WINDOWS`, `OS.MAC`, `OS.LINUX`, `OS.SOLARIS`, `OS.AIX`, `OS.OTHER`. You can pass several. ```java @EnabledOnOs({OS.LINUX, OS.MAC}) // runs only on Linux or macOS @DisabledOnOs(OS.WINDOWS) // runs everywhere except Windows ``` ## @EnabledOnJre / @DisabledOnJre / @EnabledForJreRange Keyed on the **Java runtime version**. `@EnabledOnJre(JRE.JAVA_17)` runs only on Java 17; `@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)` runs on a range. ## @EnabledIfSystemProperty / @DisabledIfSystemProperty Keyed on a **JVM system property** - a key/value set on the JVM, e.g. via `-Dci=true`. You provide two elements: `named` (the property name) and `matches` (a regular expression the value must match). ```java @EnabledIfSystemProperty(named = "env", matches = "ci") ``` Important subtlety: with the `@Enabled...` form, if the named property is **absent**, the condition is treated as *not matched*, so the test is **disabled**. With the `@Disabled...` form, an absent property means *not matched*, so the test stays **enabled**. (Same absent-property semantics apply to the environment-variable pair below.) ## @EnabledIfEnvironmentVariable / @DisabledIfEnvironmentVariable Identical mechanics to the system-property pair, but reads an **OS environment variable** (e.g. `INTEGRATION_TESTS`) instead of a JVM property. Again `named` + `matches` (regex). ```java @EnabledIfEnvironmentVariable(named = "INTEGRATION_TESTS", matches = "true") ``` ## @EnabledIf / @DisabledIf The **general-purpose** escape hatch. You supply the **name of a method** that returns `boolean`; JUnit invokes it and uses the result. The method can be a no-arg instance method, or (commonly) a static method referenced by its fully-qualified name, so it can be shared. `@EnabledIf("customCondition")` runs the test only when `customCondition()` returns `true`; `@DisabledIf("...")` skips when the method returns `true`. Use this when none of the built-in dimensions (OS/JRE/property/env) capture your condition. ```java @EnabledIf("databaseReachable") void hitsRealDb() { ... } boolean databaseReachable() { return /* ping */; } ``` ## Cross-cutting behavior - **Re-evaluated every run.** The condition is checked each execution, so a test self-enables when the environment changes - the big advantage over static `@Disabled`. - **Self-documenting skips.** Each annotation contributes a reason like 'Disabled on OS: MAC' or 'matches did not match' to the skipped-test report. - **Method or class scope.** Put them on a class to gate every test in it. - **Absent property/var = not matched** for the `@Enabled...` forms - a common gotcha worth remembering.

  • With @EnabledIfSystemProperty(named="x", matches="y"), what happens if system property x is not set at all?
    Absent counts as 'not matched', so the @Enabled form disables (skips) the test. The @Disabled form would leave it enabled.
  • How does @EnabledIf differ from @EnabledIfSystemProperty?
    @EnabledIf delegates to an arbitrary boolean-returning method, so it can express any custom condition, whereas @EnabledIfSystemProperty only matches a named JVM property against a regex.

saying these in an interview costs you the question

  • Confusing system properties (-Dkey=val, JVM) with environment variables (OS-level)
  • Forgetting that matches is a regex, not an exact-equality string
  • Assuming an absent property leaves an @Enabled test running (it disables it)
  • Thinking @EnabledIf takes a lambda/boolean literal rather than a method name

context