skip to content

Nested & Dynamic Tests

Structuring tests with @Nested inner classes and generating them at runtime with @TestFactory. Interviewers ask to see whether you know Jupiter beyond static @Test methods.

on this pageshow

explore

questions

15

In JUnit 5, what is a DynamicTest object, how do you construct one, and how does it differ at runtime from a plain method annotated with @Test?

level: juniorimportance: must knowfreq 36%

answer

  1. DynamicTest.dynamicTest(name, Executable)
  2. Executable.execute() throws Throwable
  3. DynamicNode = DynamicTest | DynamicContainer
  4. Created at runtime, not at discovery
  5. No annotations on a dynamic test

basics

~20 s

A DynamicTest is a test case created at runtime as an object, via DynamicTest.dynamicTest(displayName, executable) — a display-name string plus a lambda holding the assertions. Unlike a @Test method it is not discovered by annotation scanning and carries no annotations of its own.

solid answer

~60 s

`DynamicTest` is a runtime test case represented as an **object** rather than an annotated method. You build one with the static factory: ```java DynamicTest.dynamicTest("rejects negative amount", () -> assertThrows(...)); ``` The first argument is the display name shown in the report; the second is an `Executable` — a functional interface whose `execute()` may throw any `Throwable`, so assertions and checked exceptions work naturally in the lambda. `DynamicTest` and `DynamicContainer` both extend `DynamicNode`, so a group of them forms a tree of test nodes. The key runtime difference: a `@Test` method exists at **discovery** time — JUnit scans annotations, builds the test plan, and IDEs and filters can see it before anything runs. A dynamic test only exists once the generating code has executed. Consequences follow from that: you cannot put annotations such as `@Disabled` or `@Tag` on a dynamic test, and name-based filters cannot select an individual one. What you gain is that the number, names and behaviour of the cases can be computed from data — files, database rows, a spec document.

code

java · 10 lines
java
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

@TestFactory
List<DynamicTest> palindromeCases() {
    return List.of(
        dynamicTest("racecar is a palindrome",
            () -> assertTrue(Strings.isPalindrome("racecar"))),
        dynamicTest("kotlin is not a palindrome",
            () -> assertFalse(Strings.isPalindrome("kotlin"))));
}

go deeper

for a junior

Recall the factory call, the two arguments, and that these tests are created while the suite runs rather than found by annotation scanning.

for a middle

Add the DynamicNode hierarchy, the Executable throws-Throwable detail, and the concrete trade-off: no annotations and no discovery-time visibility in exchange for data-driven cases.

for a senior

Emphasise reporting and diagnosis — independent pass/fail per case, discriminating display names, URI test sources for navigation — and when generated cases beat one big test method.

for a principal

Frame it as a choice about where the test set is defined: in code at discovery time versus in data at runtime, and the tooling costs (filtering, identity stability) that choice imposes on the pipeline.

## The two kinds of test in Jupiter JUnit Jupiter distinguishes **static** tests from **dynamic** ones. A static test is the familiar `@Test` method. It is found by scanning classes for annotations during the *discovery* phase, before any test code runs. The full test plan — every test's identity, name and tags — is known up front. That is what lets an IDE show the tree before executing, and what lets tag or name filters select a subset. A dynamic test is a plain Java object of type `org.junit.jupiter.api.DynamicTest`, produced by code while the suite is already running. Nothing about it exists at discovery time: the engine knows only that some generator will produce nodes, and the actual cases appear as they are generated. ## Constructing one ```java DynamicTest t = DynamicTest.dynamicTest("palindrome: racecar", () -> assertTrue(isPalindrome("racecar"))); ``` Two pieces: - **displayName** — a `String`, free-form, shown in reports and IDEs. There is no method name to fall back on, so this string *is* the test's identity for a human reader. Make it discriminating: `"case 3"` is useless in a failure report; `"rejects amount -5 with ILLEGAL_AMOUNT"` is not. - **executable** — an `org.junit.jupiter.api.function.Executable`, a functional interface with `void execute() throws Throwable`. Because it declares `Throwable`, you can call anything inside without wrapping checked exceptions, and JUnit's assertions work exactly as in a `@Test` method. A thrown `AssertionError` fails that dynamic test only; siblings still run. There is also an overload taking a `URI testSourceUri`, which tells the IDE where the test "lives" (a file, a class, a method), so double-clicking a failure can navigate somewhere sensible. Without it, navigation lands on the generating method. ## DynamicNode: the type hierarchy ``` DynamicNode (abstract) ├── DynamicTest — a leaf: display name + Executable └── DynamicContainer — a branch: display name + children (DynamicNodes) ``` Because containers hold nodes, and a node may itself be a container, generated tests can form an arbitrarily deep tree rather than one flat list. Both node types are created only through their static factory methods; they are immutable value-ish objects with no annotations, no lifecycle of their own, and no way to attach metadata. ## What you give up 1. **No annotations.** `@Disabled`, `@Tag`, `@Timeout`, `@RepeatedTest` and friends target methods; a `DynamicTest` is an object, so none of them apply. To disable a generated case you filter it out of the generator; to time-box it you use `assertTimeout` inside the executable. 2. **Not visible before execution.** Discovery-time filters (by tag, by test name pattern) select the *generating method*, all-or-nothing. You cannot ask a build to run only the 17th generated case by name. 3. **No per-case Jupiter lifecycle callbacks.** `@BeforeEach`/`@AfterEach` are bound to the generating method, not to each generated node. 4. **Index-based identity.** Unique IDs are positional (`#1`, `#2`, …), so inserting a case shifts the identity of later ones. ## What you gain The test *set itself* becomes data-driven at runtime. Concrete wins: - One case per file in a fixtures directory, discovered when the suite runs — add a file, get a test, with no code change. - One case per row returned by a query, or per entry in a specification document. - Cases whose count depends on the environment (every registered implementation of an interface, every enum constant, every endpoint in an OpenAPI document). - Named sub-checks inside a scenario, each reported and failing independently rather than the first `assertEquals` aborting the rest. That last point is worth stressing: replacing ten sequential assertions in one `@Test` with ten dynamic tests changes the report from "one red test" to "three red cases, seven green", which is far better diagnostic information. ## Runtime behaviour Each dynamic test is reported to the platform as its own test node: it gets start and finish events, appears separately in the IDE tree and in XML/HTML reports, and its failure does not stop siblings. So although Jupiter's *extension* callbacks do not fire per node, platform-level listeners do observe each one — which is exactly why IDEs can display them individually while `@BeforeEach` still runs only once. ## Minimal complete picture ```java @TestFactory Collection<DynamicTest> palindromes() { return List.of( dynamicTest("racecar is a palindrome", () -> assertTrue(isPalindrome("racecar"))), dynamicTest("kotlin is not a palindrome", () -> assertFalse(isPalindrome("kotlin")))); } ``` The generated objects are the tests; the surrounding method merely hands them to the engine.

  • Can you put @Disabled or @Tag on a single dynamic test?
    No. Those annotations apply to methods and classes, and a dynamic test is an object created at runtime, after discovery-time annotation processing is over. To exclude a case you filter it out in the generating code; to tag a group, tag the generating method, which tags all of its cases together.
  • How do you make a failure in a generated case navigable in the IDE?
    Use the overload of DynamicTest.dynamicTest that takes a URI test source — for example a file URI for the fixture that drove the case, or a `class:`/`method:` URI. Without it the IDE can only point at the generating method, which is unhelpful when a hundred cases came from a hundred files.

A @Test method is a printed exam question; a DynamicTest is a question generated on the spot from the day's data — real and gradeable, but it did not exist when the paper was typeset.

saying these in an interview costs you the question

  • Calling a dynamic test "a @Test created by reflection" — it is an object, not a method, and never carries annotations.
  • Believing a failing dynamic test aborts the remaining generated cases.
  • Thinking tag or name filters can select an individual generated case.
  • Giving every generated case the same display name, making failures unidentifiable.
  • Assuming the executable cannot throw checked exceptions — Executable.execute declares Throwable.

context

open as a page

In JUnit 5, what does annotating an inner test class with @Nested do, and why must that class be non-static?

level: juniorimportance: must knowfreq 50%

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.

open as a page

A developer reports that in JUnit 5 their @BeforeEach method runs only once even though the factory method they wrote produced twenty executable test cases at runtime. Why does that happen, and how do you give each generated case its own fresh setup?

level: middleimportance: must knowfreq 32%

basics

~20 s

Dynamic tests are generated at runtime, so Jupiter's per-test callbacks bind to the generating method, not to each generated case: @BeforeEach and @AfterEach run once around the whole factory. Put setup and teardown inside each executable, typically via a shared wrapper helper.

open as a page

For a test method declared inside a JUnit 5 @Nested class, in what order do the enclosing and nested lifecycle callbacks run, how many objects does the framework create, and what does it take to declare @BeforeAll inside that nested class?

level: middleimportance: must knowfreq 40%

basics

~20 s

@BeforeEach runs outside-in (outer then nested), @AfterEach inside-out. With the default per-method lifecycle each test gets a fresh outer instance plus a fresh nested instance. @BeforeAll must be static, which needs Java 16+ or @TestInstance(PER_CLASS) on the nested class.

open as a page

What is a @TestFactory method in JUnit 5, and how does its execution differ from a plain @Test method?

level: middleimportance: must knowfreq 35%

basics

~20 s

A @TestFactory method is not a test; it is a factory invoked at runtime that returns dynamic test nodes, which JUnit then executes. Cases are produced during execution rather than fixed at discovery, so the number and names of tests can depend on runtime data.

open as a page

Do @BeforeEach and @AfterEach run around each case produced by a JUnit 5 @TestFactory method? Explain the lifecycle and how you would get per-case setup and teardown.

level: seniorimportance: must knowfreq 30%

basics

~20 s

No. @BeforeEach and @AfterEach wrap the factory method itself — once — not each generated case. Test-level extension callbacks are likewise not invoked per case. Per-case setup must be written inside each generated case's own executable, or wrapped by a helper you apply when building them.

open as a page

In JUnit 5, how do you generate one runtime test case per element of an input stream or iterator without hand-building a list of case objects, and what does the laziness of that stream buy you?

level: middleimportance: should knowfreq 24%

basics

~10 s

Use DynamicTest.stream(inputStreamOrIterator, displayNameGenerator, testExecutor): it maps each input to a case named by the generator and executed by the ThrowingConsumer. The stream is consumed lazily as cases run, and JUnit closes it afterwards.

open as a page

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?

level: middleimportance: should knowfreq 30%

basics

~20 s

A 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.

open as a page

Which return types are legal for a JUnit 5 @TestFactory method, and what happens at runtime if the method returns something else?

level: middleimportance: should knowfreq 26%

basics

~20 s

It may return a single DynamicNode, or a Stream, Collection, Iterable, Iterator, or array of DynamicNode (DynamicTest and DynamicContainer are the two node kinds). Anything else throws a JUnitException at runtime — the compiler cannot catch it, so the test fails when the factory runs.

open as a page

When JUnit 5 test-generating code produces dozens of runtime cases, how do you group them into a tree instead of one flat list, and how are those generated nodes identified in reports and IDEs?

level: seniorimportance: should knowfreq 20%

basics

~20 s

Wrap groups with DynamicContainer.dynamicContainer(displayName, children), where children are DynamicNodes — including further containers, so nesting is arbitrarily deep. Generated nodes get positional unique IDs like [dynamic-container:#1]/[dynamic-test:#2], and an optional URI test source drives IDE navigation.

open as a page

How do @DisplayName values compose across the levels of a JUnit 5 nested test tree, and what does @DisplayNameGeneration with DisplayNameGenerator.IndicativeSentences change about the reported names?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Each container and method carries its own display name, and reports render them as a tree: outer > nested > test. IndicativeSentences instead concatenates the enclosing display names with the test's into one sentence. Display names are cosmetic only — filtering and reruns use unique IDs built from class and method names.

open as a page

You need one JUnit 5 test case per record in a data file that is read at runtime, generated by a @TestFactory method. How do you build it, and what pitfalls do you guard against?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Read the file into a stream, map each record to a dynamic test with a descriptive display name, and return the stream — JUnit closes it and consumes it lazily. Guard against an empty file silently producing zero tests, non-descriptive case names, shared state across cases, and reading from the source tree instead of the classpath.

open as a page

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%

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.

open as a page

Your team wants to turn a directory of 400 JSON fixture files into individual JUnit 5 test cases generated at runtime, one per file. What operational trade-offs would you weigh before committing to that, and how would you keep the suite maintainable?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

You gain a case per file with zero code churn as fixtures are added. You lose discovery-time visibility: no per-case tags or disabling, no name-based selection, positional unique IDs that break rerun-failed and flaky history, and no framework-managed per-case setup. Keep it by naming cases after files, sorting, supplying source URIs, and owning isolation explicitly.

open as a page

When would you choose runtime-generated JUnit 5 tests from a @TestFactory over statically declared test methods in a large suite, and what does that choice cost you in tooling and CI?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Choose a factory only when the set of cases is unknowable until runtime — files on disk, registered implementations, data fetched by the build. The costs are no pre-run visibility, no per-case lifecycle, index-based identities that break rerun history, and a green build when the input is empty.

open as a page