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?
answer
- boolean, no args, not private
- static unless @TestInstance(PER_CLASS)
- FQN form Class#method for shared conditions
- evaluated per element, before instance creation
- missing method = configuration error, not skip
basics
~20 sThe 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.
solid answer
~50 s`@EnabledIf("someCondition")` names a **condition method** that Jupiter invokes to decide enablement. The rules: - **Signature:** returns `boolean` (or `Boolean`), takes no parameters. - **Visibility:** must not be `private`; package-private, protected or public is fine. - **Static-ness:** must be `static` under the default `PER_METHOD` test-instance lifecycle, because the condition is evaluated *before* a test instance is created. With `@TestInstance(Lifecycle.PER_CLASS)` an instance method is allowed, since the instance already exists. - **Location:** by default the method is looked up in the test class (including inherited methods); an external one is referenced by fully qualified name with `#`, e.g. `@EnabledIf("com.acme.Conditions#databaseAvailable")`. - **Timing:** evaluated each time the annotated element's enablement is decided — once for a container when placed on a class, once per test method when placed on a method. Use it when enablement depends on logic the built-ins cannot express: several variables combined, a config file's contents, a probe for a running service, a licence check. Keep it cheap and side-effect free.
code
java · 25 linesimport org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledIf;
class DockerBackedTest {
@Test
@EnabledIf(value = "dockerAvailable", disabledReason = "no docker socket")
void startsContainer() { }
static boolean dockerAvailable() {
return Conditions.DOCKER; // memoised, not probed per call
}
}
final class Conditions {
static final boolean DOCKER = new java.io.File("/var/run/docker.sock").exists();
static boolean docker() { return DOCKER; }
}
class SharedConditionTest {
@Test
@EnabledIf("com.acme.Conditions#docker")
void alsoNeedsDocker() { }
}go deeper
Recall the signature rules — boolean, no arguments, static, not private — and show a small example.
Explain why static is required (conditions run before instance creation), the PER_CLASS exception, and the fully qualified Class#method form for shared conditions.
Cover evaluation frequency and its cost, memoising probes, no side effects, meta-annotation wrapping, and preferring the built-in annotations when they suffice.
Decide where enablement logic belongs at all: a reusable ExecutionCondition extension with a deactivatable class name for suite-wide policy, versus a one-off predicate; and track gated tests so environment drift does not quietly erase coverage.
## What the annotation does ```java @Test @EnabledIf("dockerAvailable") void usesTestcontainers() { } static boolean dockerAvailable() { return new File("/var/run/docker.sock").exists(); } ``` `@EnabledIf`/`@DisabledIf` are the escape hatch of the condition family: instead of matching an OS, a Java version or one property value, you supply arbitrary Java that returns `true`/`false`. They are the method-based form introduced in JUnit 5.7; an older *script*-based `@EnabledIf` that evaluated JavaScript existed in 5.5 and was removed in 5.6 — worth knowing so old blog snippets do not confuse you. ## The rules, and why each exists **Returns boolean, takes no arguments.** Anything else produces an `ExtensionConfigurationException` at execution time — a configuration failure, not a skipped test. Recent Jupiter versions also allow the method to accept an `ExtensionContext` parameter, which is useful for inspecting the element or configuration parameters; the zero-arg form is the one to quote by default. **Must not be private.** Jupiter reflects on the class hierarchy to find the method; private methods are excluded by design so that the contract is explicit. **Static unless PER_CLASS.** The condition is evaluated *before* the test instance is constructed — that is the whole point of a condition, so that constructors, `@BeforeEach` and injected parameters are never touched for a test that will not run. With the default `PER_METHOD` lifecycle there is no instance yet, so the method must be `static`. Declaring `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` makes one instance per class, created before conditions on methods are evaluated, so an instance method becomes legal. Getting this wrong is the classic error message: the method "must be static" unless the PER_CLASS lifecycle is used. **Lookup by name or FQN.** A bare name is resolved against the test class, including superclasses. An external condition uses `fully.qualified.ClassName#methodName`, letting a team share one `Conditions` utility class across the suite. The external method follows the same signature and visibility rules and must be static. **When it is evaluated.** Conditions are `ExecutionCondition` extensions, and the engine asks them for each container and each test as it walks the tree. On a class it runs once for the container — if it returns false, the whole class including `@BeforeAll` is skipped. On a method it runs once per test method, before the instance is created. It is *not* cached across elements: a class with twenty annotated methods calls the method twenty times, which is why an expensive probe (opening a socket, querying a database) should memoise its result in a static field. ## When to prefer it - **Compound logic:** "run only when `CI=true` *and* `REGION` starts with `eu-` *and* the feature flag file says so" — the built-in annotations AND together but cannot express OR or read files. - **Capability probes:** is Docker reachable, is a native library loadable, does the machine have enough memory, is a port free. - **Config-driven suites:** enablement derived from a properties file or a Spring profile resolved at build time. And when *not* to: if the built-in annotation expresses it (`@EnabledOnOs`, `@EnabledIfEnvironmentVariable`), use that — it is self-documenting in the report, produces a good default reason and cannot go stale the way a hand-written method can. ## Practical cautions 1. **Keep it fast and pure.** It runs per element, sometimes hundreds of times, and under parallel execution potentially concurrently. Network probes belong behind a memoised static. 2. **No side effects.** Starting a container from a condition method looks clever and breaks reporting: conditions run outside the lifecycle, so nothing tears that container down. 3. **Give a reason.** `disabledReason` is available here too; a skipped test whose reason is just the method name teaches the next reader nothing. 4. **Prefer a meta-annotation.** Wrap `@EnabledIf("com.acme.Conditions#dockerAvailable")` in `@RequiresDocker` so the intent is visible at the call site and the FQN string exists once. 5. **A typo is a hard error, not a skip.** If the named method cannot be found, Jupiter fails the test with a configuration exception — useful, because a silently-skipped suite is far worse. ## Relationship to custom ExecutionCondition extensions `@EnabledIf` is the lightweight version of writing your own `ExecutionCondition`. Reach for a full extension when the logic needs to be reusable with parameters, wants access to the extension context and store, or should participate in the deactivation mechanism by class name. For a one-off predicate in a single suite, the method-based annotation is the right amount of ceremony.
- Why must the condition method be static by default, and what exactly changes with @TestInstance(PER_CLASS)?Conditions are evaluated before the test instance is constructed, so under the default PER_METHOD lifecycle there is no object on which to call an instance method. With PER_CLASS, Jupiter creates a single instance for the class before evaluating method-level conditions, so an instance method becomes callable and the static requirement is lifted.
- The condition method opens a TCP connection to check whether a service is up. What is wrong with that in a large class?The method is invoked once per annotated element, not once per class, so a class with many gated tests probes the service repeatedly and slows the run; under parallel execution the probes also happen concurrently. Memoise the result in a static field or a lazily-initialised holder so the probe happens once per JVM, and keep the method free of side effects since nothing tears down what a condition starts.
- What happens if the method name in @EnabledIf is misspelled?Jupiter cannot resolve it and fails with a configuration exception for that element rather than skipping it. That is deliberate: an unresolvable condition is a bug in the test setup, and failing loudly is far safer than silently disabling tests, which is how gated suites rot unnoticed.
saying these in an interview costs you the question
- Saying the condition method may be private
- Expecting an instance method to work under the default PER_METHOD lifecycle
- Believing the method can take the test's arguments or return a String reason
- Assuming the result is cached per class, so an expensive probe runs only once
- Starting or mutating shared resources inside the condition method