skip to content

Why must a class annotated @Nested in JUnit 5 be a non-static inner class, and what does that imply for fields that the enclosing class's setup methods initialize?

level: middleimportance: should knowfreq 44%

answer

  1. inner class holds Outer.this
  2. outer instance built first, then inner bound to it
  3. outer fields visible to nested tests
  4. fresh outer instance per nested test
  5. static nested class = no enclosing instance

basics

~20 s

Because a non-static inner class instance holds a reference to an enclosing instance. JUnit builds the outer instance, runs its setup on it, then builds the inner instance bound to it — so nested tests can read and use the outer object's fields directly. A static nested class has no enclosing instance and is not treated as @Nested.

solid answer

~50 s

`@Nested` relies on Java's **inner class** semantics: a non-static inner class instance always carries a reference to an instance of its enclosing class. That is precisely the mechanism JUnit needs, because a nested test is supposed to run *inside* the outer fixture. For each test in a nested class under the default lifecycle, Jupiter constructs the **outer** instance, applies the outer `@BeforeEach`, then constructs the **inner** instance bound to that outer object and applies the inner `@BeforeEach`. The nested test can therefore read the outer instance's fields — they are ordinary fields of the enclosing object, already initialized by the outer setup. Consequences worth stating: each nested test gets a *fresh* outer instance too, so mutations do not leak between tests; a nested class marked `static` is simply not a `@Nested` test class in the intended sense and loses access to outer instance state; and because there is no enclosing instance for a static context, class-level `@BeforeAll` inside a nested class is the awkward case.

code

java · 23 lines
java
class ShoppingCartTest {

    private Cart cart;                     // outer instance field

    @BeforeEach
    void createCart() {
        cart = new Cart();                 // runs on the outer instance
    }

    @Nested
    class WithOneItem {                    // non-static: bound to the outer instance

        @BeforeEach
        void addItem() {
            cart.add(new Item("book", 10));  // outer field, already initialized
        }

        @Test
        void totalIsItemPrice() {
            assertEquals(10, cart.total());
        }
    }
}

go deeper

for a junior

Know that @Nested classes are inner (non-static) classes and that they can use the outer class's fields set up in @BeforeEach.

for a middle

Explain the enclosing-instance mechanic, the two-object construction per test, and that both instances are fresh under the default lifecycle.

for a senior

Add the consequences: isolation across the tree, shadowing pitfalls, why nested @BeforeAll is the constrained case, and what changes if the outer class is PER_CLASS.

for a principal

Weigh the readability cost of state living at several levels and set conventions — distinct field names per level, add rather than redefine, prefer separate classes when the link to the outer fixture is not real.

## The Java mechanic underneath In Java, a class declared inside another class is either **static nested** or **non-static inner**. The difference is an implicit reference: an inner class instance holds a hidden pointer to an instance of its enclosing class (reachable as `Outer.this`), and it therefore cannot exist without one. A static nested class is just a top-level class that happens to live in another class's namespace — no enclosing instance, no access to the outer object's fields. `@Nested` in JUnit Jupiter is built on the inner-class form on purpose. The point of a nested test class is to run a scenario **inside** the context established by the enclosing class, and "inside" is expressed exactly by the enclosing-instance link. ## What Jupiter does per nested test Under the default per-method lifecycle, running one test method that lives in a `@Nested` class requires two objects: 1. Construct the **outer** class instance. 2. Run outer-class instance post-processing and the outer `@BeforeEach` methods on it. 3. Construct the **inner** class instance, bound to that outer instance. 4. Run the inner `@BeforeEach` on it. 5. Run the test method on the inner instance. 6. Unwind: inner `@AfterEach`, then outer `@AfterEach`. Both objects are discarded afterwards. Two consequences follow immediately: - **The outer fixture is visible.** A field of the outer class, assigned in the outer `@BeforeEach`, is fully readable from the nested test — no plumbing, no passing objects around. This is the entire ergonomic payoff of the pattern. - **The outer fixture is fresh per test.** Because a new outer instance is created for every nested test, state written by one nested test cannot reach another one through outer fields. Isolation is preserved across the hierarchy, not just within a class. ## Shadowing and readability If the inner class declares a field with the same name as an outer field, the inner one shadows it; `Outer.this.name` reaches the outer one explicitly. This is a real source of confusion in deeply nested test classes — two levels each with a `subject` field, and the reader must track which one the assertion is talking about. The practical guidance is to name fields at each level distinctly, and to prefer *adding* state at inner levels over redefining it. ## What happens if the class is static Marking the nested class `static` removes the enclosing instance. Jupiter does not treat a `static` nested class as an `@Nested` test class in the intended sense: the enclosing lifecycle hooks no longer wrap it as an inner scenario, and the outer instance's fields are unreachable because there is no outer instance to reach. If you find yourself wanting `static`, what you actually want is a separate top-level test class — which is a perfectly good outcome and often the clearer one. ## Why class-level hooks inside a nested class are awkward `@BeforeAll`/`@AfterAll` are, by default, static methods — and a non-static inner class historically could not declare static members at all. That is why a `@BeforeAll` inside a `@Nested` class either requires `@TestInstance(Lifecycle.PER_CLASS)` on that nested class (making the hook an instance method on a single shared instance) or a modern-enough Java language level that permits static members in inner classes together with a JUnit version that supports it. The root cause is the same enclosing-instance rule discussed here. ## Interaction with the enclosing class's instance lifecycle The lifecycle setting of the outer class and of each nested class are independent. If the outer class is `@TestInstance(PER_CLASS)`, one outer instance serves all tests in the tree — including nested ones — so outer fields now persist across nested tests, and the isolation argument above no longer holds. That combination is occasionally what you want (one expensive fixture, many nested read-only scenarios) but it must be a deliberate choice, because it makes every nested class a potential source of cross-test leakage. ## The interview-grade summary "`@Nested` needs a non-static inner class because JUnit binds the nested test instance to an enclosing instance; that binding is what lets a nested test use the fields the outer setup prepared. Under the default lifecycle both the outer and the inner instance are recreated for every test, so the layering gives you shared *setup* without shared *state*." Adding the shadowing caveat and the `@BeforeAll` consequence shows you have written non-trivial nested tests rather than read about them.

  • If a nested test mutates a field of the enclosing class, can another nested test see that mutation?
    Not under the default per-method lifecycle: a new outer instance is constructed for every test method anywhere in the tree, so outer fields start fresh each time. It changes if the outer class is annotated @TestInstance(PER_CLASS), because then a single outer instance serves the entire tree and mutations do carry across nested tests — which is exactly the leakage risk you take on with that annotation.
  • What happens if you mark the nested class static?
    It loses its enclosing instance, so it can no longer read the outer object's fields, and JUnit does not treat it as a nested test class bound to the outer fixture. In practice the cleanest response is to make it a separate top-level test class, since a static nested class has none of the properties that motivated @Nested in the first place.

A room inside a building: the room only exists as part of some building, and everything the building provides — power, heating — is available inside it without being re-installed.

saying these in an interview costs you the question

  • Believing @Nested works on static nested classes and simply reuses the outer class's statics
  • Thinking one outer instance is shared by all nested tests under the default lifecycle
  • Assuming outer fields must be passed into the nested class explicitly
  • Confusing field shadowing between levels with the outer field being reassigned
  • Claiming the nested class inherits from the outer class — it is composition through an enclosing instance, not inheritance

context