How does assertAll behave with nested assertAll groups, and what are the precise semantics of its single-vs-multiple-failure reporting?
answer
- 0 fail pass; 1 fail rethrow unchanged; 2+ fail MultipleFailuresError
- Catches ANY Throwable, not just AssertionError
- Nested assertAll = inner error collected as one outer failure
- Sequential, single-thread, no early exit
- Overloads: varargs / Collection / Stream
basics
~20 sYou can nest an assertAll inside another. If only one check fails, assertAll rethrows that exact failure; if several fail, it throws one MultipleFailuresError bundling them. Nesting groups related checks and keeps the failure tree organized.
solid answer
~50 sassertAll collects the Throwable from each Executable, then decides: zero failures -> pass; exactly one -> rethrow that original Throwable unchanged (so an assertEquals failure stays an AssertionFailedError, not wrapped); two or more -> throw a single MultipleFailuresError (opentest4j) aggregating them all, with each failure's message and stack trace preserved. A lambda inside assertAll can itself call assertAll, producing nested groups; an inner MultipleFailuresError propagates up as that inner Executable's failure and is collected like any other, so the outer report nests it. assertAll runs sequentially on the calling thread and does not catch unrelated unchecked exceptions specially — any Throwable a lambda throws (including a NullPointerException) is collected as a failure for that check. Because every lambda always runs, assertAll is unsuitable when a check left earlier state inconsistent; it is purely about aggregating independent verifications, not orchestrating fail-fast control flow.
code
java · 13 lines// Nested groups: inner assertAll's failure is collected as one
// failure of the outer group, nested under its heading.
assertAll("address",
() -> assertAll("home",
() -> assertEquals("NYC", home.city()),
() -> assertEquals("10001", home.zip())),
() -> assertAll("work",
() -> assertEquals("SF", work.city()),
() -> assertEquals("94105", work.zip()))
);
// Stream overload: one check generated per element.
assertAll(items.stream().map(i -> () -> assertTrue(i.valid())));go deeper
Aware that assertAll groups can be nested and that several failures show up together; details not expected.
Knows the 0/1/2+ failure outcomes and that any Throwable is collected, not just AssertionError.
Explains single-failure rethrow vs MultipleFailuresError, nesting composition, sequential execution, and the Collection/Stream overloads.
Treats assertAll as an aggregation primitive in test-architecture terms: structures nested headed groups to mirror the domain, articulates why it is not control flow, and reasons about how the rethrow semantics interact with tooling and custom failure handling.
## Precise reporting semantics `assertAll` (org.junit.jupiter.api.Assertions) iterates the supplied `Executable`s, invoking each and catching any `Throwable` into a list. Then: 1. **Zero failures** -> returns normally; the test passes. 2. **Exactly one failure** -> it **rethrows that original Throwable unchanged**. An `assertEquals` mismatch surfaces as the same `AssertionFailedError` (a subclass of `AssertionError`) you would get without assertAll; a stray `NullPointerException` surfaces as that NPE. There is no wrapping when only one thing failed. 3. **Two or more failures** -> it throws a single **`MultipleFailuresError`** (from the **opentest4j** library, the shared test-result API JUnit 5 builds on). This error holds the list of collected failures; the test report renders each with its own message and stack trace. Key nuance: assertAll catches **any `Throwable`**, not just `AssertionError`. If a lambda throws a `NullPointerException` or `IndexOutOfBoundsException`, that is collected as that check's failure — which is precisely why dependent checks (where a later lambda blows up because of earlier state) are a poor fit. ## Nesting assertAll Because each argument is an `Executable` (a lambda), and a lambda can contain *any* code, a lambda may itself call `assertAll`: ```java assertAll("address", () -> assertAll("home", () -> assertEquals("NYC", home.city()), () -> assertEquals("10001", home.zip())), () -> assertAll("work", () -> assertEquals("SF", work.city()), () -> assertEquals("94105", work.zip())) ); ``` How it composes: the inner `assertAll("home", ...)` runs its two checks. If both inner checks fail, the inner call throws a `MultipleFailuresError`; **from the outer assertAll's perspective that inner error is simply the Throwable thrown by the "home" Executable**, so it is collected as one failure of the outer group. The outer report therefore nests the inner group's failures under its heading. Nesting is a way to **organize related sub-checks** and keep large verification blocks readable, with headings forming a path. ## Execution model - **Sequential, single-threaded:** assertAll runs the lambdas in order on the calling thread. There is no parallelism, no reordering, no retry. - **Always runs every lambda:** there is no early exit. This is the defining property and the source of both its benefit (full failure picture) and its hazard (no short-circuit for dependent checks). - **Stream overload:** besides varargs and `(String heading, Executable...)`, there are overloads accepting `Collection<Executable>` and `Stream<Executable>`, useful for generating checks dynamically (e.g. one per item). ## Design-level implications - Prefer **several headed groups / nested groups** over one flat grab-bag so the failure tree maps to your domain structure. - Treat assertAll as an **aggregation primitive, not control flow.** If correctness of check N depends on check N-1, use sequential asserts or split the test; do not rely on assertAll to stop. - The **single-failure-rethrow** rule means tooling and custom failure handlers see the genuine assertion error when only one thing is wrong — assertAll does not obscure the common case. ## Terms recap - **Executable:** JUnit 5 functional interface, `void execute() throws Throwable`. - **AssertionFailedError:** opentest4j error a failed JUnit assertion throws (subtype of AssertionError). - **MultipleFailuresError:** opentest4j aggregate thrown when assertAll has 2+ failures. - **opentest4j:** the open testing result library JUnit 5's assertions are built on.
- If exactly one assertion in an assertAll group fails, what exception type propagates?The original Throwable, unchanged — e.g. the AssertionFailedError from the failed assertEquals. assertAll only produces a MultipleFailuresError when two or more checks fail; with one it rethrows as-is.
- What overloads of assertAll exist beyond varargs Executables?An overload taking a String heading plus Executables, and overloads accepting a Collection<Executable> or a Stream<Executable>. The Collection/Stream forms are handy for generating one check per element dynamically.
- How does a failure inside a nested assertAll appear to the outer group?The inner assertAll throws (a single Throwable if one inner check failed, or a MultipleFailuresError if several did); the outer assertAll collects that as the one failure of the lambda that called the inner assertAll, nesting it under the outer heading.
saying these in an interview costs you the question
- Claiming a single failure is still wrapped in MultipleFailuresError (it is rethrown unchanged).
- Thinking assertAll only catches AssertionError and lets NPEs escape (it catches any Throwable).
- Assuming nested assertAll runs in parallel or short-circuits the outer group.
- Believing assertAll provides fail-fast control flow rather than pure aggregation.