In a JUnit 5 test class, what happens if you declare the nested test class as static — with or without @Nested — and how does that differ from a non-static inner class?
answer
- static = no enclosing instance = no outer fixture
- discovered standalone as Outer$Inner
- @Nested on static ≠ nested container
- inner without @Nested = silently never runs
- static member class fine for helpers, not tests
basics
~20 sA static nested class is not a child container. It behaves as an independent top-level test class (reported as Outer$Inner): no enclosing instance, so no outer fields, no outer @BeforeEach, no inherited class-level configuration. @Nested on it does not make it nested.
solid answer
~50 sMaking the class `static` breaks the link the nesting model depends on. Jupiter only resolves **non-static** member classes marked `@Nested` as children of the enclosing container. A `static` member class is instead treated like a top-level test class that merely lives inside another file — build-tool classpath scanning discovers it as `OuterTest$InnerTest` and runs it standalone. Consequences: the outer class's fields and `@BeforeEach` never run for it, and class-level configuration on the outer class (registered extensions, `@TestInstance` mode, `@DisplayNameGeneration`) is not inherited, because there is no container relationship. The confusing symptom is the discovery asymmetry: run the whole suite via Gradle/Maven and the static class's tests execute; select the outer class in the IDE and they do not appear, because the outer container resolves only `@Nested` inner classes as children. Adding `@Nested` to a static class does not fix it — depending on the JUnit 5.x version it is ignored or reported as a configuration problem.
code
java · 26 linesclass OrderTest {
private Order order;
@BeforeEach
void setUp() {
order = new Order();
}
@Nested
class WhenPaid { // child container: sees `order`
@Test void isClosed() { }
}
static class WhenShipped { // standalone test class OrderTest$WhenShipped
@Test void hasTracking() { } // outer @BeforeEach never runs
}
class WhenCancelled { // no @Nested: never discovered at all
@Test void isRefunded() { }
}
static Order anOrder() { // legitimate use of static members here
return new Order();
}
}go deeper
Know the one-line rule: @Nested needs a non-static inner class; static means it is a separate test class with no outer setup.
Explain the missing enclosing instance, the loss of inherited class-level configuration, and that an inner class without @Nested is not discovered at all.
Cover the discovery asymmetry between selecting the outer class and full-suite scanning, and how silently-skipped tests are detected in CI.
Treat it as a suite-integrity concern: how the team detects test-count regressions, and conventions for what may live as a static member of a test file.
## The two kinds of member class Java distinguishes a *static nested class* (`static class Inner`) from an *inner class* (`class Inner`). Only the inner class instance holds a hidden reference to an enclosing instance. JUnit 5's nesting model is built entirely on that reference, so the two shapes get completely different treatment. ## What Jupiter does with each **Non-static inner class + `@Nested`** — resolved as a child container of the enclosing test class. The full onion lifecycle applies: outer `@BeforeEach` first, outer fields visible, extensions and `@TestInstance` mode inherited, and the report shows it under the outer class in the tree. **Static member class** — not a child container. Jupiter's "is this a test class?" predicate accepts top-level classes and static member classes but rejects inner classes, so a static member class with `@Test` methods is a perfectly valid *standalone* test class. Build tools that scan compiled classes (Gradle's and Maven's test discovery walk the class files) will find `OuterTest$InnerTest` and run it on its own. There is no enclosing instance, therefore no outer fixture, no inherited class-level annotations, and the outer class's `@BeforeEach` never executes for it. **Static member class + `@Nested`** — still not a child container. The annotation cannot create an enclosing instance out of nothing. Historically this was silently ignored (the class still ran standalone during a full-suite run); newer JUnit 5 versions surface it as a warning or a reported configuration problem. The lesson is to treat `@Nested` on a `static` class as a bug regardless of version. **Inner class with no `@Nested`** — the worst case: not a valid standalone test class (inner classes are excluded from discovery) and not resolved as a child either. Its tests silently never run and nothing warns you. This is a real source of "we thought that was covered" incidents; a coverage check or a deliberate failing test is how teams catch it. ## The discovery asymmetry that confuses people The symptom that sends people looking is inconsistent behaviour between the IDE and CI: - **Select the outer class and run it** — the platform issues a class selector for `OuterTest`; resolution walks its `@Nested` inner classes only, so the static class's tests are absent from the run. - **Run the whole module** — the build tool scans every compiled class file, `OuterTest$InnerTest` passes the test-class filter, and its tests execute as a separate class. So the same tests appear or vanish depending on how the run was launched, and their reported parent is a class name with a `$` in it rather than a readable nested node. ## Why this matters beyond pedantry 1. **Fixture surprise.** A static class that was refactored out of a nested one keeps compiling (it just cannot reference outer instance fields), so people convert fields to statics to make it compile — turning per-test state into shared mutable state and introducing order-dependent flakiness. 2. **Lost configuration.** Extensions registered on the outer class (a database extension, a clock extension) stop applying, often producing confusing NPEs or real connections in tests that used to be stubbed. 3. **Filtering ergonomics.** Selecting a static member class from a build tool means quoting `OuterTest$InnerTest`, and the `$` needs escaping in some shells — whereas a `@Nested` container is addressed through the platform's unique ID, not a class-name filter. ## When a static nested class is actually the right choice It is fine — even idiomatic — for **non-test helpers** inside a test file: a static fixture builder, a test double implementation, a small record used only by this test. Those have no reason to see outer instance state. Keep `@Test` methods out of them, or accept that they will run as an independent class. ## The rule to state in an interview "`@Nested` requires a non-static inner class. `static` turns it into an independent test container: no outer instance, no outer setup, no inherited configuration, discovered only by full-suite scanning. Static member classes in a test file should hold helpers, not tests."
- How would you catch a nested class whose tests silently stopped running?Watch the executed-test count, not just the pass/fail result: a drop in total tests between builds is the signal. Coverage reports catch it too, since the covered lines disappear. Some teams add a build check that fails when the test count falls, and reviewers are trained to look for inner classes missing @Nested.
- Is it ever fine to have a static nested class inside a JUnit 5 test class?Yes, for non-test code: fixture builders, small test doubles, parameter records, or a stub implementation of an interface. Those need no enclosing instance and keep the test file self-contained. The rule is that they should not carry @Test methods, otherwise they become an accidental standalone test class.
saying these in an interview costs you the question
- Saying @Nested works the same on a static class
- Claiming a static nested class inherits the outer class's @BeforeEach or registered extensions
- Assuming an inner class without @Nested still runs because it contains @Test methods
- Thinking the static class's tests never run at all — they do run under full-suite scanning, just standalone
- Converting outer fields to static to make a static nested class compile, without noticing the shared-state consequence