A group of integration tests can only run when a Docker daemon is reachable. Compare guarding them with a runtime precondition check from JUnit's Assumptions class against a declarative execution condition, and say when each is appropriate.
answer
- assumption = runtime probe, after instantiation
- declarative condition = pre-instantiation, on the signature
- socket probe cannot be a static annotation
- @BeforeAll + cached probe = one skip per class
- CI must fail, not skip, when the environment should exist
basics
~20 sAn assumption probes at runtime inside the test or its setup, so it can perform real checks like opening a socket, but the class is instantiated first and the skip is only visible in the report. A declarative condition is evaluated before instantiation, is visible on the signature, and suits statically knowable facts such as an OS or an environment variable.
solid answer
~60 s**Runtime assumption** — `assumeTrue(dockerReachable(), "no Docker daemon")`, typically in `@BeforeAll` or `@BeforeEach`. It can execute arbitrary probing code: connect to the daemon socket, check an API version, verify a pulled image. It runs after the test instance and fixtures exist, so you pay for construction, and the reason lives inside the method body where a reader must go looking. **Declarative condition** — an annotation such as an enabled-if-environment-variable check, or a custom `ExecutionCondition` extension. It is evaluated before the test instance is constructed, so nothing is built for a test that will not run; the reason appears on the method or class signature; and it can be reused across classes as a meta-annotation. Rule of thumb: statically knowable facts (OS, JRE version, environment variable, system property, CI flag) → declarative. Facts that require doing work to discover (is the daemon actually answering, does this image exist, is this fixture dataset present) → assumption, ideally in `@BeforeAll` so the probe runs once and its result is cached. Either way the skip must carry a reason and must be counted, otherwise a misconfigured agent silently drops the whole integration layer while the build stays green.
code
java · 33 linesimport static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.net.InetSocketAddress;
import java.net.Socket;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class ContainerBackedRepositoryTest {
private static Boolean dockerUp;
@BeforeAll
static void requireDocker() {
if (dockerUp == null) {
dockerUp = probe("localhost", 2375, 300);
}
assumeTrue(dockerUp, "Docker daemon not reachable on :2375");
}
private static boolean probe(String host, int port, int timeoutMs) {
try (var s = new Socket()) {
s.connect(new InetSocketAddress(host, port), timeoutMs);
return true;
} catch (Exception e) {
return false;
}
}
@Test
void savesAggregate() {
// ...
}
}go deeper
Know that both approaches exist, that they result in skipped rather than failed tests, and that a reason message should always be given.
Contrast timing (before instantiation versus during execution), what each can inspect, and where the reason is visible; recommend @BeforeAll for expensive probes.
Argue the hybrid: cheap declarative filter plus a real runtime probe, and insist that CI must fail rather than skip when it is meant to provide the environment.
Treat skips as a coverage-governance problem — measure them, keep reasons specific, and decide deliberately between providing the environment, virtualising it, or deleting the tests.
## The shared goal Both mechanisms exist to answer: this test cannot meaningfully run here, so do not fail the build over it. Both produce a not-executed outcome that build tools display as skipped. They differ in *when* the decision is made, *what* it can inspect, and *how visible* it is. ## Runtime assumption Calling `Assumptions.assumeTrue(...)` throws `org.opentest4j.TestAbortedException` when the condition fails, aborting from that point. Its strengths: - **Arbitrary probing.** Deciding whether Docker is usable is not a boolean you can read from the environment. You have to try: open the daemon socket or ping the HTTP API, with a short timeout, and treat any failure as unavailable. Only executable code can do that. - **Access to fixtures.** The check can use the injected clients, configuration objects and helpers the test class already builds. - **Fine granularity.** In a parameterized test, an assumption can skip individual argument sets that do not apply here while the rest run. Its weaknesses: - **Late.** The test instance and its `@BeforeEach` chain up to that point are constructed first. If constructing the fixture is itself expensive — or itself hangs when Docker is missing — the guard came too late. - **Invisible.** Nothing on the method signature says the test is conditional; a reader must open the body. Tooling that lists tests cannot tell you why a run skipped things without reading messages. - **Repetition.** Placed per test, the probe runs per test unless memoised. The standard mitigation is to put the probe in `@BeforeAll` with a cached static result, so it happens once per class, aborts the whole container, and produces a single clear skip line. ## Declarative condition Jupiter's conditional execution is an extension point: `ExecutionCondition` returns enabled or disabled with a reason, and built-in annotations cover common cases — operating system, architecture, JRE range, system property, environment variable, and a custom-method form that calls a static predicate you write. Strengths: - **Evaluated before instantiation.** No test instance, no fixture construction, no cost for something that will not run. - **Visible.** The condition and its reason sit on the method or class, so the constraint is documentation. - **Composable.** Wrap it in a custom annotation and apply that single annotation across the codebase; changing the policy is a one-file edit. - **Reported as disabled with a reason** supplied by the condition, which reads better in reports than a generic assumption message. Weaknesses: the custom-method form runs a static predicate with limited context (no test instance, no injected fixtures), so a probe needing the fixture is awkward; and a probe with I/O in a condition still costs time, just earlier. ## Choosing for the Docker case In practice, a hybrid is best. Use a declarative condition for the coarse, cheap decision — for example, a custom annotation that consults a system property or environment flag that CI sets when containers are supported, so laptops without Docker skip instantly with a visible reason. Then, inside the classes that survive that filter, use a `@BeforeAll` assumption performing the real probe with a short timeout, since only executable code can confirm the daemon is truly answering. Crucially, do not let CI use the same escape hatch. If the pipeline is supposed to have Docker, the tests must fail — loudly — when it is missing. The clean way is a build-level or environment-level assertion: a single test, or a startup check, that fails when a variable marking a container-capable environment is set but the daemon does not answer. That converts a silent, green mass skip into one actionable red failure, and it is the difference between a guard and a hiding place. ## Costs to keep in mind Every guard is coverage you are choosing not to run. A suite where hundreds of tests skip locally trains developers to trust a green local run that verified far less than they think. Track skip counts over time, make skip reasons specific, and periodically read them. When a whole category is permanently skipped everywhere, the honest options are to provide the environment (containers in CI, a service virtualisation layer) or delete the tests — not to keep them as green decoration.
- Why can't the Docker check simply be a declarative annotation that reads an environment variable?Because an environment variable records intent, not reality: it says someone believes Docker should be present, which is a different claim from the daemon answering right now. A stale variable on a machine where Docker was uninstalled produces a hard failure instead of a skip. The variable is a fine coarse filter, but confirming availability needs executable probing with a timeout, which is what a runtime assumption gives you.
- How do you prevent this guard from silently disabling the integration tests in CI?Make the environment assertion explicit and separate. Have CI set a flag meaning 'this environment is supposed to support containers', and add one test or startup check that fails when the flag is set but the probe fails. Then a broken agent produces a single actionable red failure rather than a green build with hundreds of silent skips, and you can additionally assert a minimum executed-test count or alert on skip-count jumps.
A declarative condition is a sign on the door saying 'closed on Sundays'; an assumption is walking in and finding the lights off. The sign is cheaper and visible from outside, but only the walk-in tells you whether the power is actually on.
saying these in an interview costs you the question
- Treating an assumption as free — forgetting it runs after the test instance and fixtures are constructed.
- Using a runtime skip in CI for an environment CI is supposed to provide, so a broken agent yields a green build.
- Claiming an environment variable check proves a service is reachable.
- Repeating an expensive probe in every test instead of caching it in @BeforeAll.
- Never reviewing skip counts, so permanently skipped categories become invisible dead tests.