If a JUnit 5 test method is annotated @Disabled, which lifecycle callbacks still run — @BeforeAll, @BeforeEach, @AfterEach, @AfterAll — and is the test class instantiated? How does the answer change when the whole class is disabled?
answer
- conditions evaluated per node, before that node's setup
- disabled method: @BeforeAll still runs, no instance, no @BeforeEach
- disabled class: nothing — no @BeforeAll/@AfterAll, no instantiation
- all methods disabled ≠ free — fixture still starts
- TestWatcher.testDisabled fires instead
basics
~20 sFor a disabled method the class container still runs, so @BeforeAll and @AfterAll execute, but no instance is created for that method and its @BeforeEach/@AfterEach are skipped. For a disabled class nothing runs at all — no @BeforeAll, no @AfterAll, no instantiation.
solid answer
~40 sDisabling is evaluated **per node**, before that node's own setup. **Method-level `@Disabled`:** the class container is still enabled, so `@BeforeAll` runs (and `@AfterAll` afterwards) even if every method in the class happens to be disabled. For the disabled method itself, JUnit stops before creating the test instance: no constructor, no `@BeforeEach`, no `@AfterEach`, and no `beforeEach`/`afterEach` extension callbacks. Any extension implementing `TestWatcher` gets `testDisabled` instead. **Class-level `@Disabled`:** the container itself is skipped, so `@BeforeAll` and `@AfterAll` do **not** run and the class is never instantiated; class-level extension callbacks such as `beforeAll` are not invoked either. The practical consequence: a class-level `@Disabled` is the way to avoid paying for expensive class fixtures — starting a container, opening a connection pool — whereas disabling every method one by one still pays the `@BeforeAll` cost for nothing.
code
java · 15 linesclass ShippingTest {
@BeforeAll
static void startContainer() { /* RUNS - container is enabled */ }
@BeforeEach
void seed() { /* NOT run for the disabled test */ }
@Test
@Disabled("blocked by PROJ-12")
void quotesOvernightDelivery() { /* skipped: no instance created */ }
@AfterAll
static void stopContainer() { /* RUNS */ }
}go deeper
Know the headline: a disabled test's own setup does not run, and a disabled class runs nothing at all.
Explain the per-node condition evaluation and the @BeforeAll asymmetry between method-level and class-level disabling.
Use it operationally: move disabling up to the class when the fixture is the expensive or broken part, and reason about which callbacks a skip suppresses.
Connect it to suite cost and reporting semantics — skipped containers versus skipped tests, and what your health metrics actually count.
## The rule: conditions are evaluated per node, before setup JUnit Jupiter builds a tree of *descriptors*: an engine, containers (test classes and `@Nested` classes) and tests (methods). Before executing any node, the engine evaluates its **execution conditions**; `@Disabled` is implemented as a built-in condition. If the node is disabled, the engine skips it and everything below it, and never runs that node's own setup. From that single rule everything follows. ## Method-level @Disabled The class container passed its own condition check, so the container executes: - `@BeforeAll` runs — **even if every method in the class is disabled**. People are often surprised by this: ten disabled tests still pay for the Testcontainers start in `@BeforeAll`. - `@AfterAll` runs at the end of the container. The disabled method's own subtree is skipped entirely: - the test class is **not instantiated** for it (with the default per-method lifecycle, an instance is created per test; a skipped test creates none, so no constructor and no field initialisers run for it); - `@BeforeEach` and `@AfterEach` do not run; - extension callbacks `beforeEach`, `postProcessTestInstance`, parameter resolution and `afterEach` do not run; - an extension implementing `TestWatcher` receives `testDisabled(extensionContext, reason)`. This is why a disabled test cannot leave state behind and cannot fail in setup: none of its setup exists. ## Class-level @Disabled The container fails its condition, so nothing under it happens: - **no `@BeforeAll`, no `@AfterAll`** — this is the important difference; - no instantiation, no `@BeforeEach`/`@AfterEach`, no `@Nested` classes; - class-level extension callbacks (`beforeAll`, `afterAll`) are not invoked. Extensions registered by `@ExtendWith` on the class are not given a chance to do work. So if a heavyweight fixture is the thing that is broken, disable at the class level. Disabling all the methods individually is a false economy: the fixture still starts. ## `@TestInstance(Lifecycle.PER_CLASS)` With per-class lifecycle, one instance is shared by all tests and `@BeforeAll` may be a non-static instance method. The container is executed, so that shared instance *is* created and `@BeforeAll` runs, regardless of how many methods are disabled. Constructor side effects therefore still happen — one more reason not to do work in test constructors. ## Practical implications - **Cost.** Disabled methods are almost free; disabled classes are entirely free; a class full of disabled methods is not free. - **Reasoning about leaks.** If a resource opened in `@BeforeEach` is not closed, a disabled test is not a suspect — its `@BeforeEach` never ran. - **Reporting.** Skipped tests are reported per method; a disabled class shows up as a skipped container, which some report formats summarise differently. If you rely on skip counts as a health metric, know which shape you are looking at. - **Conditional variants behave identically.** `@EnabledOnOs`, `@DisabledIfSystemProperty` and custom `ExecutionCondition` implementations are the same mechanism, so all of the above applies to them too.
- Every test in a class is disabled and the class has an expensive @BeforeAll. What should you change?Move the annotation to the class. A class-level @Disabled skips the container itself, so @BeforeAll and @AfterAll never run and the fixture is never built. With method-level annotations the container is still considered enabled, so the expensive setup executes and is then thrown away.
- Does an extension registered with @ExtendWith on a disabled test method still get its callbacks?Its per-test callbacks do not run — beforeEach, parameter resolution and afterEach are all part of the skipped subtree. What it can observe is TestWatcher.testDisabled, which reports the skip along with the reason. Class-level callbacks such as beforeAll still run, since the container itself executed.
saying these in an interview costs you the question
- Claiming @BeforeEach runs before a disabled test and is then discarded
- Assuming @BeforeAll is skipped when all test methods in the class are disabled
- Saying a disabled class still runs @AfterAll for cleanup
- Believing the test instance is constructed and then thrown away for a disabled method