skip to content

How do you decide whether a large JUnit 5 test class should be organized with @Nested groupings or split into several separate top-level test classes?

level: principalimportance: should knowfreq 22%

answer

  1. nest by state, split by subject
  2. shared fixture is the only real justification
  3. depth ≤ 2 (3 exceptionally)
  4. big file = merge hotspot, coarse sharding unit
  5. PER_CLASS / statics are inherited temptations

basics

~20 s

Nest when the tests share one subject and differ only by state, so each nested level adds a slice of fixture. Split when the groups share no fixture, the file grows unreadable, or the groups are owned or evolve separately. Keep nesting two levels deep at most.

solid answer

~60 s

The deciding question is **does the group share fixture with its parent?** `@Nested` earns its cost when tests exercise one subject in several states: the outer class builds the subject, each nested class moves it into a scenario, and the report reads like a specification. If a candidate group would ignore the outer fixture entirely, it is a separate class wearing a nested costume — split it. Secondary factors I weigh: - **Depth.** Two levels is comfortable, three is the ceiling; beyond that a reader cannot reconstruct the state without scrolling through several `@BeforeEach` methods. - **File size and ownership.** A 900-line test file is a merge-conflict magnet even when perfectly structured; splitting by subject reduces churn. - **Tooling.** Nested containers are addressed through platform unique IDs; class-name filters need the `$` form. Test-name-based CI sharding is coarser for one big class. - **Parallelism.** Configuration such as `@Execution` is inherited by nested classes, so a whole file shares one concurrency decision; separate classes can differ. Default: nest by *state*, split by *subject*.

go deeper

for a junior

Say that nesting groups tests that share the same setup, and that very large files are worth splitting; you are not expected to reason about sharding.

for a middle

Frame it as fixture reuse, mention a sensible depth limit, and note that groups with unrelated setup belong in their own class.

for a senior

Add the operational angle: file churn, filtering ergonomics, reporting, and the isolation risks of PER_CLASS or static fixtures inside big nested files.

for a principal

Present it as a suite-design policy with an explicit decision procedure, weighing comprehension, ownership boundaries, parallelism and CI sharding, and state the review guardrails you enforce.

## The core criterion: shared fixture `@Nested` exists to let a child container reuse and extend the enclosing instance's setup. That is the entire mechanism, so it is also the entire justification. Ask of each proposed group: *would its `@BeforeEach` build on what the outer one produced?* - **Yes** — nest. The outer class constructs the subject; the nested class puts it in a state (`when the cart is empty`, `when the item is out of stock`, `when the token has expired`); the methods assert outcomes. Each level states only its delta, and the reported tree reads as a specification. - **No** — split. A group that ignores the outer fixture gains nothing from nesting and pays for it: readers must still scan the outer setup to be sure it is irrelevant. A useful smell: if a nested class starts by *undoing* something the outer `@BeforeEach` did, the nesting is wrong. ## Depth discipline Nesting depth is unbounded in JUnit, but comprehension is not. At depth three, the state a test operates on is defined across four `@BeforeEach` methods spread over hundreds of lines. Reviewers stop verifying the setup and start trusting it, which is exactly when a stale fixture slips through. Practical rule: **two levels by default, three only when the third is genuinely a sub-state of the second.** If you want depth four, you have found a second subject — extract it. ## File size, ownership and churn Structure is not the only cost of a large file. A test class that every feature touches becomes a merge-conflict hotspot, a slow file to open, and a place where unrelated changes review together. Even a beautifully nested 900-line class is usually better as three files of 300 lines split by subject — the nesting quality is unchanged, the collaboration cost drops. Also weigh ownership: if two nested groups are maintained by different teams or move at different speeds, separate files give independent history, independent blame and independent review. ## Tooling consequences - **Selection and filtering.** A `@Nested` container is a child node addressed by the platform's unique ID. Selecting one from a build tool by class name means naming the binary form with `$`, which needs escaping in some shells. Selecting a top-level class is trivial. If your team routinely runs sub-groups from the command line, that friction is real. - **Sharding.** CI sharding usually distributes *classes*. One enormous nested class is one indivisible unit of work; splitting it lets the shards balance. - **Reporting.** Some CI dashboards flatten the tree and show only the method display name, losing the scenario context — which is when `@DisplayNameGeneration` with indicative sentences becomes worth turning on. - **Parallel execution.** Execution-mode configuration declared on the enclosing class is inherited by nested classes, so the whole file shares one concurrency policy. If one group needs `SAME_THREAD` because it touches a static or an external resource while the rest can run concurrently, separate classes express that more honestly. ## Shared mutable state Under the default per-method lifecycle, isolation is free: fresh outer and nested instances per test. That safety disappears the moment someone adds `@TestInstance(PER_CLASS)` (inherited by all nested classes) or a `static` field to speed something up. Large nested files are where those shortcuts accumulate, because the expensive fixture is visible and tempting to hoist. Guard that boundary in review: expensive shared setup belongs in an extension with explicit lifecycle, not in a static field inside a deep nest. ## A decision procedure 1. Group the tests by the **state** they need. If the groups form a hierarchy over one subject → `@Nested`, one level per state distinction. 2. If the groups differ by **subject**, collaborator, or transport → separate top-level classes. 3. Cap depth at two (three exceptionally). 4. Cap file size by judgement (a few hundred lines); when exceeded, split by subject before flattening structure. 5. Give the outer class the subject name, each nested class a `when…`/`given…` name, each method an outcome name. 6. Keep isolation defaults; treat `PER_CLASS` and static fixtures as decisions requiring justification. ## What a strong answer sounds like Name the shared-fixture criterion first, then depth, then the collaboration and tooling costs, and finish with the isolation risk. Weak answers argue purely from aesthetics ("nesting is cleaner") or from a blanket rule ("never nest") without connecting to fixture reuse, which is the only thing the mechanism actually provides.

  • A nested class begins by resetting or replacing something the outer @BeforeEach created. What does that tell you?
    That the grouping is not a sub-state of the outer fixture, so the nesting is providing negative value — the reader has to understand setup that is then discarded. Either move the shared construction down into the nested classes that actually want it, or extract this group into its own top-level class. Undoing parent setup is one of the clearest signals to split.
  • How does a large nested test class interact with CI sharding and parallel test execution?
    Most CI sharding distributes whole classes, so one big nested class is a single indivisible chunk that can dominate a shard's runtime. Execution-mode configuration on the enclosing class is inherited by nested containers, so all groups share one concurrency policy; if one group must run single-threaded, splitting it out lets the rest run concurrently.

Nested classes are like nested folders: they help when each level narrows the same thing, and hurt the moment you are using them to store two unrelated projects under one root.

saying these in an interview costs you the question

  • Nesting purely for visual grouping when the group ignores the outer fixture
  • Arguing nesting depth does not matter because JUnit allows any depth
  • Hoisting expensive setup into static fields or PER_CLASS inside a deep nest without acknowledging the isolation loss
  • Claiming nested containers behave identically to top-level classes for filtering and sharding
  • Refusing to nest at all and duplicating the same setup across a dozen flat tests

context