skip to content

When two lambdas inside a JUnit 5 Assertions.assertAll group fail, what exception does the test report and what does its message contain? And what does the framework throw if only one of them fails?

level: middleimportance: should knowfreq 38%

answer

  1. opentest4j MultipleFailuresError extends AssertionError
  2. heading + "(N failures)" + one line each
  3. individual failures = suppressed exceptions
  4. exactly one failure → rethrown unwrapped
  5. OutOfMemoryError not collected

basics

~20 s

Two or more failures are aggregated into org.opentest4j.MultipleFailuresError, whose message shows the heading plus the failure count and lists each failure (they are also attached as suppressed exceptions). If exactly one executable fails, that original throwable is rethrown unchanged.

solid answer

~50 s

assertAll collects the throwables from all executables. If the list is empty the group passes. If it holds two or more, JUnit throws `org.opentest4j.MultipleFailuresError` — an `AssertionError` subclass whose message is the heading followed by `(N failures)` and then one line per failure; each collected throwable is also added as a suppressed exception, so IDEs and CI reports can show individual stack traces. The special case worth knowing: if exactly one executable failed, assertAll rethrows *that* throwable as-is rather than wrapping it. So a single failure inside a group looks exactly like an ungrouped assertion failure — same type, same expected/actual comparison view in the IDE — which is deliberate, since wrapping would hide the useful diff. Unrecoverable errors such as `OutOfMemoryError` are not collected: they propagate immediately and the remaining executables do not run.

code

java · 7 lines
java
assertAll("order",
    () -> assertEquals(2, order.lineCount()),
    () -> assertEquals(new BigDecimal("10.00"), order.total()));

// org.opentest4j.MultipleFailuresError: order (2 failures)
//   org.opentest4j.AssertionFailedError: expected: <2> but was: <1>
//   org.opentest4j.AssertionFailedError: expected: <10.00> but was: <0.00>

go deeper

for a junior

Know the name MultipleFailuresError and that the report lists every failure with the heading and a count.

for a middle

Add the single-failure rethrow, that any Throwable is collected, and that individual failures ride along as suppressed exceptions.

for a senior

Discuss report readability and CI parsing — headings that identify the group, unrecoverable errors that short-circuit, and why assumptions do not belong inside a group.

for a principal

Talk about failure reporting as a contract with tooling: opentest4j types exist so IDEs and reporters stay decoupled from the engine, and grouping conventions should preserve that signal at suite scale.

## The aggregation type `assertAll` does not invent its own exception. It throws `org.opentest4j.MultipleFailuresError`, which comes from opentest4j — the small, framework-neutral test-result API that JUnit 5 uses so that IDEs and reporting tools can understand failures without depending on JUnit internals. `MultipleFailuresError` extends `AssertionError`, so from the runner's point of view the test simply failed. Its message is built from two pieces: 1. the heading you passed to `assertAll` (or a default when you passed none), and 2. the number of collected failures, e.g. `user dto (2 failures)`. Underneath, each collected failure is rendered on its own line, typically as the exception type plus its message — `org.opentest4j.AssertionFailedError: expected: <36> but was: <35>`. The individual throwables are also attached to the aggregate as **suppressed** exceptions (`Throwable.addSuppressed`), which is how IDEs and CI report parsers get real stack traces for each one instead of only the summary text. ## The one-failure special case The implementation counts what it collected. If exactly one throwable was collected, assertAll rethrows that throwable directly instead of wrapping it in a `MultipleFailuresError`. This matters in practice for two reasons: - IDEs recognise `AssertionFailedError` and offer the side-by-side expected/actual comparison view; wrapping would lose it. - The failure of a grouped test with one broken check reads exactly like a plain assertion failure, so grouping costs nothing in diagnosability when only one thing is wrong. Because the rethrow keeps the original type, a single non-assertion throwable — say a `NullPointerException` escaping one lambda — surfaces as that exception, not as an assertion error. ## What is collected and what is not An `Executable` may throw any `Throwable`, and assertAll treats anything thrown as a failure of that executable: assertion errors, checked exceptions, unchecked exceptions. There is one deliberate exception: JUnit does not swallow *unrecoverable* errors. `OutOfMemoryError` is rethrown immediately, aborting the group, because continuing to run code after the JVM has run out of memory is pointless and the collected report would be untrustworthy anyway. One subtlety worth knowing: an aborted assumption inside a lambda throws `TestAbortedException`, which assertAll treats like any other throwable. If it is the only one collected it is rethrown as-is and the test is reported as *aborted/skipped*; if it is collected alongside real failures it becomes one entry in the aggregate and the test is reported as failed. That makes conditional skipping inside a group unreliable — put the assumption before the group instead. ## Reading the report When you see `MultipleFailuresError` in CI output, read the heading first (which group), then the failure count, then the per-failure lines. If your reporting tool only prints the top-level message and you are missing the individual stack traces, the suppressed exceptions are where they live — most modern reporters print them; some older ones need configuring. ## Nesting A nested `assertAll` that fails throws its own `MultipleFailuresError`; to the outer group that is just one throwable from one executable, so the outer aggregate contains it as a single entry with the inner list nested inside its message. The report therefore keeps the structure of your groups rather than flattening everything.

  • Why does JUnit rethrow the original throwable when only one grouped executable fails, instead of always wrapping?
    Wrapping would replace a recognisable AssertionFailedError with an aggregate, and IDEs key their expected/actual comparison view off the original type and its value fields. Rethrowing unwrapped keeps single-failure diagnostics identical to an ungrouped assertion, so grouping never degrades the common case.
  • Can an executable throw a checked exception, and what happens if it does?
    Yes — Executable.execute() declares throws Throwable, so a lambda can call methods that throw checked exceptions without try/catch. If one escapes, assertAll collects it exactly like an assertion failure: it becomes one entry in the aggregate, or is rethrown as-is if it is the only failure.

saying these in an interview costs you the question

  • Claiming assertAll always throws MultipleFailuresError, even for a single failure
  • Thinking each grouped failure appears as its own test result in the report
  • Assuming only AssertionError is collected and other exceptions propagate immediately
  • Believing an assumption failure inside the group cleanly skips the test
  • Not knowing where per-failure stack traces are (suppressed exceptions)

context