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.
answer
- OnJre = exact list, ForJreRange = inclusive span
- min/max default to lowest/highest known
- unknown newer JDK -> JRE.OTHER -> outside every range
- open-ended enum min silently skips on new JDKs
- 5.12+: integer minVersion/maxVersion
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.
solid answer
~50 s`@EnabledOnJre(JRE.JAVA_17, JRE.JAVA_21)` enables the test on exactly those feature releases — an enumeration, not a floor. `@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)` enables it on the inclusive span; omitting `min` means "from the lowest supported", omitting `max` means "up to the highest known". Both have `@DisabledOnJre` / `@DisabledForJreRange` twins. The sharp edge is `JRE.OTHER`. The `JRE` enum only contains the releases known to your JUnit version, and anything newer is detected as `OTHER`. `OTHER` is not inside any named range, so a test annotated `@EnabledForJreRange(min = JRE.JAVA_17)` — written to mean "17 and up" — is *skipped* the day the build moves to a JDK newer than the enum. It looks like the test broke, but it silently stopped running. Since JUnit 5.12 the range annotations also accept integer `minVersion`/`maxVersion`, which compare numerically and are therefore future-proof; that is the form to prefer for open-ended "N and later" gates.
code
java · 22 linesimport org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
class JreGatedTest {
@Test
@EnabledOnJre({JRE.JAVA_17, JRE.JAVA_21})
void exactlyThoseTwoReleases() { }
@Test
@EnabledForJreRange(min = JRE.JAVA_17) // skipped on a JDK newer than the enum
void seventeenAndLater_fragile() { }
@Test
@EnabledForJreRange(minVersion = 17) // JUnit 5.12+: numeric, future-proof
void seventeenAndLater_robust() { }
@Test
@DisabledForJreRange(maxVersion = 16,
disabledReason = "needs sealed-type pattern matching")
void invertedGateAlsoSurvivesUpgrades() { }
}go deeper
Distinguish the exact-list annotation from the range annotation and know a gated test is skipped, not failed.
Explain min/max defaults, the JRE.OTHER behaviour on newer JDKs, and the integer minVersion/maxVersion form that fixes it.
Argue for inverting open-ended gates so they fail open, keeping JUnit current, and monitoring skipped counts so silent coverage loss is detectable.
Treat version gates as a maintenance liability with a decay mode; set a policy for how JDK upgrades are validated, including a check that the skipped-test population does not grow after a toolchain bump.
## Two shapes of the same idea Both annotations live in `org.junit.jupiter.api.condition` and gate a test on the Java feature release the tests are executing on (as reported through the JVM's version, not the compiler's `--release`). ```java @EnabledOnJre({JRE.JAVA_17, JRE.JAVA_21}) // exactly 17 or exactly 21 @EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21) // 17, 18, 19, 20, 21 @DisabledOnJre(JRE.JAVA_17) // everything except 17 @DisabledForJreRange(max = JRE.JAVA_11) // disabled up to and including 11 ``` The enumeration form is right for "this bug only reproduces on 17" or "this API behaves differently on exactly these releases". The range form is right for "needs a language or API feature added in N" — the far more common case. Defaults for the range: `min` defaults to the lowest JRE the annotation knows, `max` to the highest, so `@EnabledForJreRange(max = JRE.JAVA_11)` reads "everything up to 11". ## The JRE.OTHER trap `JRE` is a Java enum: `JAVA_8`, `JAVA_9`, ... up to whatever release existed when your JUnit version shipped, plus `OTHER`. Jupiter detects the current feature release and maps it onto a constant; anything it does not recognise becomes `OTHER`. Now consider a project on JUnit 5.9 (whose enum stops at, say, JAVA_19) with: ```java @Test @EnabledForJreRange(min = JRE.JAVA_17) // intent: "Java 17 and later" void usesSealedInterfaces() { } ``` The day CI upgrades to JDK 21, the detected JRE is `OTHER`, which is not within `[JAVA_17, highest-known]`, so the condition disables the test. The suite stays green; the test simply stops running, and the report shows it skipped with a reason mentioning the JRE version. Because green builds are not investigated, this can hide for months — which is exactly why it is an interview question: it is a *silent coverage loss*, the worst failure mode a test gate can have. Symmetrically, `@DisabledForJreRange(max = JRE.JAVA_11)` keeps working on new JDKs (OTHER is outside the disabled span, so the test runs), and `@DisabledOnJre(JRE.JAVA_8)` behaves fine. The trap is specific to open-ended *enabling* ranges expressed with enum constants. ## The fixes 1. **Use integer versions** where available: JUnit 5.12 added `minVersion`/`maxVersion` int attributes to `@EnabledForJreRange`/`@DisabledForJreRange` (and `versions` to `@EnabledOnJre`/`@DisabledOnJre`). These compare numerically against the detected feature version, so `minVersion = 17` genuinely means "17 and later", including releases JUnit has never heard of. 2. **Keep JUnit current.** The enum trap is a symptom of a JUnit version older than the JDK it runs on; upgrading the JUnit BOM regularly makes it rare. 3. **Invert the gate.** Where semantics allow, express the rule as "disabled below N" (`@DisabledForJreRange(maxVersion = 16)`) so that unknown future releases fall on the *running* side rather than the skipped side. Failing open is the right default when the alternative is losing coverage silently. 4. **Watch skip counts.** Any CI that reports skipped-test counts — and fails or warns when the number moves — catches this class of problem generically, along with misspelled environment gates. ## Reporting and reasons A JRE-gated test that does not run is reported as skipped with a default reason naming the detected version, and all four annotations accept `disabledReason` for a human explanation. Because the gate encodes a *technical* requirement, the reason should say what the requirement is ("uses the Foreign Function API finalised in 22"), not restate the annotation. ## Related distinction worth stating These conditions test the **runtime** JVM. They do not know what bytecode level the class was compiled to, whether a preview feature is enabled (`--enable-preview`), or which toolchain the build used. If a test needs a preview feature, gating on the JRE version alone is insufficient — the flag must also be passed to the test JVM, and a method-based `@EnabledIf` probing for the capability is often the more honest gate.
- Why does @DisabledForJreRange(max = JRE.JAVA_11) survive a JDK upgrade while @EnabledForJreRange(min = JRE.JAVA_17) does not?Both compare the detected JRE against a named span, and an unrecognised new release is reported as JRE.OTHER, which lies outside every span. Being outside a *disabled* span means the test runs; being outside an *enabled* span means it is skipped. So the inverted gate fails open on unknown releases while the open-ended enabling gate fails closed and silently loses coverage.
- How would you detect that JRE gating has silently disabled tests in CI?Track the skipped-test count as a build signal: publish it from the test reports and alert or fail when it rises. Skipped tests are invisible in a green build, so the only reliable defence is making the number itself observable, which also catches misspelled environment-variable gates and forgotten @Disabled annotations.
saying these in an interview costs you the question
- Reading @EnabledOnJre(JRE.JAVA_17) as "17 and later" rather than exactly 17
- Assuming an unknown newer JDK is treated as the highest known constant
- Thinking the annotations inspect the compiler target or toolchain rather than the running JVM
- Believing a JRE-gated test that does not run shows up as passing
- Expecting @EnabledForJreRange with only min set to keep working forever without upgrading JUnit