In JUnit 5, what does annotating an inner test class with @Nested do, and why must that class be non-static?
answer
- @Nested = child container in the test tree
- non-static inner → implicit Outer.this reference
- outer.new Inner() per test method
- @BeforeEach outside-in, @AfterEach inside-out
- static nested = separate top-level container
basics
~20 s@Nested makes an inner class a child test container: its tests run under the outer class and reuse the outer class's fields and @BeforeEach setup. It must be non-static so every nested instance is bound to a live enclosing instance.
solid answer
~50 s`@Nested` marks a **non-static inner class** inside a test class as a child container in the test tree. Jupiter discovers it, and for each test inside it creates an instance of the outer class first, then the inner instance bound to it (`outer.new Inner()`). Because the inner instance carries an implicit reference to the enclosing instance, nested tests can read and extend fields the outer class set up — outer `@BeforeEach` runs before the nested one, so each nesting level adds a layer of fixture. It has to be non-static because that outer-instance link is exactly a Java *inner class* feature: a `static` nested class has no enclosing instance, so there would be nothing to inherit state from. That is also why a static nested class is treated as an independent test container rather than a child of the outer class. The practical payoff is a given/when/then shaped test tree: outer = subject, each `@Nested` = a scenario, methods = the assertions for that scenario.
code
java · 34 linesclass ShoppingCartTest {
private Cart cart;
@BeforeEach
void createEmptyCart() {
cart = new Cart();
}
@Test
void newCartIsEmpty() {
assertTrue(cart.isEmpty());
}
@Nested
@DisplayName("when one item has been added")
class WithOneItem {
@BeforeEach
void addItem() {
cart.add(new Item("book", 10));
}
@Test
void isNotEmpty() {
assertFalse(cart.isEmpty());
}
@Test
void totalEqualsItemPrice() {
assertEquals(10, cart.total());
}
}
}go deeper
Be able to say @Nested groups related tests under the outer class, that the class is a non-static inner class, and that outer setup still applies.
Add the mechanics: the outer.new Inner() instantiation chain, outside-in @BeforeEach / inside-out @AfterEach, and a fresh instance pair per test method under the default lifecycle.
Explain why the non-static requirement is a consequence of Java inner-class semantics, what happens to a static nested class instead, and how the nesting shapes the reported test tree.
Frame it as suite structure: when layered fixtures aid comprehension versus when a long nested file should be split by subject, and how nesting interacts with configuration inheritance and parallel execution.
## The problem @Nested solves A plain JUnit 5 test class is a flat list of `@Test` methods sharing one `@BeforeEach`. That works until the subject has several distinct states — an empty cart, a cart with one item, a cart already checked out. With a flat class you end up either repeating the same three setup lines in a dozen tests, or writing one `@BeforeEach` that sets up the union of every scenario and hoping each test only touches its own part. Both make tests hard to read and easy to break. `@Nested` (from `org.junit.jupiter.api`) lets you express those scenarios structurally. You declare an inner class per scenario, annotate it `@Nested`, and give it its own `@BeforeEach` that moves the subject into that state. The tests inside it then only assert. ## What Jupiter actually does During discovery, when Jupiter resolves a test class it looks at its member classes and treats every **non-static member class annotated with `@Nested`** as a nested test container — a child node in the test tree, with its own display name, its own lifecycle methods, and possibly its own `@Nested` children. During execution, with the default `PER_METHOD` test-instance lifecycle, running one test inside a nested class means Jupiter: 1. creates a fresh instance of the outermost class, 2. runs its `@BeforeEach` methods, 3. creates the inner instance bound to that outer instance (`outer.new Inner()` in Java terms), 4. runs the inner `@BeforeEach`, 5. runs the test method, 6. runs inner `@AfterEach`, then outer `@AfterEach`. So `@BeforeEach` callbacks run **outside-in** and `@AfterEach` **inside-out**, and every test method gets a brand-new outer+inner instance pair. Mutating outer state inside a nested test cannot leak into a sibling test. Class-level configuration on the enclosing class — registered extensions, `@TestInstance` lifecycle mode, `@DisplayNameGeneration` — is inherited by `@Nested` classes, so you configure once at the top. ## Why non-static is required In Java, a non-static member class (an *inner* class) instance holds an implicit reference to an instance of its enclosing class, reachable as `Outer.this`. That reference is what lets nested test code read the field the outer `@BeforeEach` just populated. A `static` nested class has no such reference; it is really a top-level class that happens to be namespaced inside another one. Jupiter's whole nesting model is built on that instance chain, so `@Nested` is defined for inner classes only. A `static` nested class is not a child container: it is discovered (by build-tool classpath scanning) as its own standalone test class named like `OuterTest$InnerTest`, with no outer fixture applied. Putting `@Nested` on a static class does not promote it — depending on the JUnit 5.x version you get silence, a logged warning, or a reported configuration problem, but never the nested behaviour you wanted. Two related declaration rules follow the same spirit: the class must not be `private`, and it must not be abstract or local/anonymous. Package-private (the default, no modifier) is the idiomatic choice. ## What you get from it - **Readable reports.** The IDE/report tree reads `ShoppingCartTest > when one item added > total equals item price`, which is close to a specification. - **Fixture layering.** Each level adds only its delta of setup; there is no giant shared `@BeforeEach`. - **Scoped helpers.** Helper methods and fields that only one scenario needs live in that scenario's class instead of polluting the whole file. - **Arbitrary depth.** `@Nested` classes can contain `@Nested` classes; in practice two or three levels is the readability ceiling. ## Common mistakes Declaring the class `static` out of habit (JUnit 4's `Enclosed` runner required static classes) is the classic error — the tests either silently stop running when you select the outer class in the IDE, or run without the outer fixture and fail with `NullPointerException`. Forgetting `@Nested` entirely is the mirror image: an inner class without the annotation is simply not discovered, and its tests quietly never run — which looks like a passing build.
- What does @Nested give you that simply extracting those tests into a separate top-level test class would not?A separate top-level class cannot see the enclosing class's fixture, so you would duplicate or extract the setup into a base class or helper. @Nested keeps the fixture layered and local: the outer @BeforeEach still runs, and the report shows the scenario as a child of the subject rather than as an unrelated class. The tradeoff is file size — one class can grow long, and at some point splitting by subject beats nesting by scenario.
- Can @Nested classes themselves contain @Nested classes, and is there a practical limit?Yes, nesting depth is unlimited and each level contributes its own lifecycle callbacks and display name, applied outside-in. In practice two or three levels is the readability limit: beyond that the setup for a given test is spread across so many @BeforeEach methods that a reader cannot reconstruct the state without scrolling. Deep trees also produce very long indicative-sentence display names.
- If a nested test mutates a field of the outer class, can it affect another test?Not with the default PER_METHOD lifecycle: Jupiter creates a fresh outer instance and a fresh inner instance for every test method, so mutations die with the test. The exception is @TestInstance(PER_CLASS) or static/shared state such as static fields or an external database, where the usual isolation concerns apply.
Think of the outer class as a stage set and each @Nested class as a scene that adds props to it: the crew rebuilds the whole set for every take, then dresses it for the scene before the actors say their lines.
saying these in an interview costs you the question
- Saying @Nested classes must be static (a habit carried over from JUnit 4's Enclosed runner)
- Claiming the enclosing class's @BeforeEach does not run for tests inside a @Nested class
- Assuming one outer instance is shared by all nested tests, so state leaks between them
- Believing an inner class is discovered as a nested container even without @Nested — it is silently ignored and its tests never run
- Declaring the nested class private, then wondering why nothing is discovered