How does JUnit 5 decide at runtime that a test is disabled, and how would you implement your own rule — for example skipping tests that need an external service when that service is unreachable? Is there a way to force such skipped tests to run anyway?
answer
- ExecutionCondition extension; @Disabled = built-in DisabledCondition
- first disabled result wins, whole subtree skipped
- evaluated before instantiation and @BeforeEach
- ConditionEvaluationResult.enabled/disabled(reason)
- junit.jupiter.conditions.deactivate=* forces disabled tests to run
basics
~20 sJupiter asks every registered ExecutionCondition extension before running a node; @Disabled is itself the built-in DisabledCondition. You implement ExecutionCondition, return ConditionEvaluationResult.disabled(reason) or enabled(reason), and register it with @ExtendWith. The config parameter junit.jupiter.conditions.deactivate switches conditions off by pattern so disabled tests run.
solid answer
~40 sDisabling is an extension point, not special-cased engine logic. Before executing any container or test, Jupiter evaluates all registered **`ExecutionCondition`** extensions for that node; the first *disabled* result wins and the node is skipped with that reason. `@Disabled` is implemented by a built-in `DisabledCondition`, and `@EnabledOnOs` and friends are the same mechanism. Your own rule is one method: ```java public class ServiceUpCondition implements ExecutionCondition { public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext ctx) { return probe() ? ConditionEvaluationResult.enabled("service reachable") : ConditionEvaluationResult.disabled("service unreachable"); } } ``` Register with `@ExtendWith(ServiceUpCondition.class)`, usually behind a composed annotation, and read your own marker annotation from `ctx.getElement()`. Conditions run before the test instance exists, so cache expensive probes in the root `ExtensionContext.Store`. To force everything to run, set the configuration parameter **`junit.jupiter.conditions.deactivate`** to a pattern — `*` deactivates all conditions including `@Disabled`.
code
java · 14 linespublic class ServiceUpCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext ctx) {
boolean up = ctx.getRoot().getStore(Namespace.GLOBAL)
.getOrComputeIfAbsent("svc.up", k -> probeOnce(), Boolean.class);
return up ? ConditionEvaluationResult.enabled("sandbox reachable")
: ConditionEvaluationResult.disabled("payment sandbox unreachable");
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(ServiceUpCondition.class)
public @interface RequiresPaymentSandbox {}go deeper
Recall that @Disabled is implemented as an extension and that JUnit 5 lets you write custom conditions.
Sketch the ExecutionCondition interface, its return type, and registration via @ExtendWith.
Cover pre-instantiation evaluation, caching probes in the Store, composing the condition into a marker annotation, and the conditions.deactivate escape hatch.
Frame it as suite policy: environment-dependence expressed declaratively, an audit run with conditions deactivated, and TestWatcher-based reporting of what is being skipped and why.
## The mechanism JUnit Jupiter builds a tree of container and test descriptors. Before executing a node it evaluates the node's **execution conditions**: every extension registered for that node that implements `org.junit.jupiter.api.extension.ExecutionCondition`. Each returns a `ConditionEvaluationResult`, either `enabled(reason)` or `disabled(reason)`. Evaluation short-circuits on the first *disabled* result, and the node — plus its whole subtree — is skipped with that reason attached. There is nothing privileged about `@Disabled`: it is implemented by a built-in condition that looks for the annotation and returns `disabled` with your string or a generated default. `@EnabledOnOs`, `@DisabledIfSystemProperty`, `@EnabledIf` are all conditions too. That uniformity is the point of the design. Because conditions run before the node executes, they run **before the test instance is constructed** and before `@BeforeEach`. A condition therefore cannot look at instance fields or anything created by setup — it sees only the `ExtensionContext`. ## Writing one Implement the single method and decide from the `ExtensionContext`: - `ctx.getElement()` gives the annotated method or class, so you can read your own marker annotation using `AnnotationSupport.findAnnotation(...)`, including attributes such as a service name or timeout. - `ctx.getConfigurationParameter("...")` reads platform configuration. - `ctx.getStore(Namespace.GLOBAL)` (or the root context's store) caches an expensive probe so you connect once per run rather than once per test. Registration options: `@ExtendWith(ServiceUpCondition.class)` on the method or class; declaratively for the whole suite through auto-registration via the `ServiceLoader` file when enabled; or — most ergonomically — bundled into a composed annotation such as `@RequiresPaymentSandbox`, so the marker and the condition travel together. A condition should be **fast and side-effect free**. It is called for every node it covers, and a slow probe multiplies across the suite; hence caching in the store. ## Forcing disabled tests to run Jupiter has a configuration parameter, **`junit.jupiter.conditions.deactivate`**, taking a pattern matched against condition class names. Matching conditions are not evaluated, so their tests execute: - `*` deactivates all conditions, including `DisabledCondition` — every `@Disabled` test runs. - `org.junit.*` deactivates the built-ins while leaving your custom ones active. It is set like any Jupiter configuration parameter: a system property, an entry in `junit-platform.properties` on the classpath, or the launcher's configuration map. This is the standard answer to "can I run the disabled tests to see what still breaks?" — useful as a periodic audit job, and a much better habit than deleting annotations by hand and forgetting to restore them. It is worth knowing the limits: deactivation switches off the *condition*, so a test disabled by an assumption inside its body is unaffected, and a test that needs an unavailable service will now fail rather than skip — which is exactly what an audit wants to see. ## Observability An extension implementing `TestWatcher` receives `testDisabled(context, Optional<String> reason)`, so a custom reporter can list every skipped test with the condition that skipped it. Combined with a deactivation audit run, that gives you a real inventory instead of a skip count. ## Common design mistakes - **Probing per test.** A network call in a condition, uncached, can dominate suite time. - **Mutating state in a condition.** Conditions may be evaluated for containers and tests; treat them as pure predicates. - **Depending on setup.** Conditions run before instantiation, so "disable if the fixture failed to load" is not expressible here — that is an assumption's job. - **Silent universal skipping.** A condition nothing satisfies makes the suite green and empty. Report on it.
- Your condition performs a network probe. How do you keep it from slowing the suite down?Cache the result in the root ExtensionContext's Store under a global namespace, so the probe runs once per launch rather than once per test. Give it a short timeout so an unreachable host fails fast instead of hanging the whole run, and treat the condition as a pure predicate — no retries, no side effects.
- Why can't a condition decide based on state created in @BeforeEach?Conditions are evaluated before the node executes, which is before the test instance is constructed and before any @BeforeEach callback runs, so that state does not exist yet. A precondition that only becomes knowable after setup belongs in an assumption inside the test body, which aborts execution and reports the test as skipped.
saying these in an interview costs you the question
- Thinking @Disabled is hard-coded engine behaviour rather than a built-in ExecutionCondition
- Believing a condition can inspect fields initialised in @BeforeEach
- Not knowing any way to run disabled tests short of editing the source
- Returning enabled/disabled by throwing an exception instead of a ConditionEvaluationResult
- Doing an uncached network call per test inside a condition