Grouped Assertions and assertAll

8 min read

Here is a problem you will hit the moment you write a test that validates more than one field of an object. You run the test, it fails on the first assertion, you fix that field, you run again, it fails on the second assertion. You fix that, run again, third assertion fails. You've now run the test three times to discover three problems that existed simultaneously. assertAll solves this by running every assertion and collecting all failures before reporting.

The problem with sequential assertions

Consider a test that validates an API response object:

@Test
void shouldReturnCorrectUserData() {
    User user = userService.findById(42);
 
    assertEquals("Alice",          user.getName());      // fails here
    assertEquals("alice@test.com", user.getEmail());     // never reached
    assertEquals("admin",          user.getRole());      // never reached
    assertTrue(user.isActive());                          // never reached
    assertNotNull(user.getCreatedAt());                   // never reached
}

If user.getName() returns "Bob", the test stops immediately. You fix the name, run again, and now discover the email was also wrong. Fix that, run a third time — the role was wrong too. You needed three CI cycles to find three bugs that were all present in the same build.

assertAll — run everything, report everything

assertAll takes a heading and an array of executable lambdas. It runs every one, collects every failure, and throws a MultipleFailuresError that lists all of them:

import static org.junit.jupiter.api.Assertions.*;
 
@Test
void shouldReturnCorrectUserData() {
    User user = userService.findById(42);
 
    assertAll("User fields",
        () -> assertEquals("Alice",          user.getName()),
        () -> assertEquals("alice@test.com", user.getEmail()),
        () -> assertEquals("admin",          user.getRole()),
        () -> assertTrue(user.isActive()),
        () -> assertNotNull(user.getCreatedAt())
    );
}

If name and role are both wrong, the output reads:

org.opentest4j.MultipleFailuresError: User fields (2 failures)
	expected: <Alice> but was: <Bob>
	expected: <admin> but was: <member>

Both failures appear in a single test run. You fix both at once and move on.

Validating an API response — a real pattern

This is where assertAll earns its place in everyday QA work. An API response has a status code, headers, and a body — you want to validate all three regardless of which fails first:

@Test
void shouldReturn200WithJsonBody() {
    Response response = given()
        .header("Authorization", "Bearer " + token)
        .get("/api/users/42");
 
    assertAll("GET /api/users/42",
        () -> assertEquals(200, response.getStatusCode()),
        () -> assertEquals("application/json", response.getContentType()),
        () -> assertNotNull(response.getHeader("X-Request-Id")),
        () -> assertAll("Response body",
            () -> assertEquals("Alice",          response.jsonPath().getString("name")),
            () -> assertEquals("alice@test.com", response.jsonPath().getString("email")),
            () -> assertEquals("admin",          response.jsonPath().getString("role"))
        )
    );
}

The nested assertAll groups status/header assertions separately from body assertions. In the failure report, the nesting is preserved — you see "Response body (1 failure)" as a sub-list under the outer heading.

Comparing with TestNG SoftAssert

TestNG users know SoftAssert — it has the same motivation:

// TestNG SoftAssert — for comparison only
SoftAssert softly = new SoftAssert();
softly.assertEquals(user.getName(), "Alice");
softly.assertEquals(user.getEmail(), "alice@test.com");
softly.assertAll(); // must call this or failures are silently ignored

assertAll in JUnit 5 is cleaner for two reasons: you don't have to instantiate anything, and you cannot forget to call the final assertAll() — there is no separate step. With TestNG's SoftAssert, forgetting softly.assertAll() at the end is a common mistake that makes all assertions silently pass. JUnit's lambda approach doesn't have that trap.

Playwright's expect.soft() and AssertJ's SoftAssertions follow the same concept — by the time you encounter them, you already understand assertAll.

When to use assertAll vs individual assertions

Use assertAll when:

  • Validating an object with multiple fields — so you see all wrong fields in one run.
  • Validating an API response — status + headers + body all at once.
  • Validating a page state — multiple elements that should all be visible/correct.

Keep individual assertions when:

  • The subsequent assertions only make sense if the first passes — for example, first asserting assertNotNull(user) and then asserting user.getName(). If user is null, the body assertion throws a NullPointerException, not a meaningful assertion failure. Use individual assertions to guard preconditions, assertAll for the payload check.
@Test
void shouldFindUser() {
    User user = userService.findById(42);
 
    // Guard: asserting non-null first makes sense here
    assertNotNull(user, "User 42 should exist");
 
    // Only runs if user is not null — safe to group now
    assertAll("User 42 fields",
        () -> assertEquals("Alice", user.getName()),
        () -> assertEquals("alice@test.com", user.getEmail())
    );
}

assertAll vs assertThrows vs assertTimeout

These three look superficially similar (all take lambda arguments) but serve completely different purposes:

  • assertAll — run multiple assertions, collect all failures.
  • assertThrows — verify that a lambda throws a specific exception.
  • assertTimeout / assertTimeoutPreemptively — verify that a lambda completes within a time limit.

They can be combined: you can put an assertThrows call inside an assertAll lambda if needed, though that is rare.

Individual vs assertAll

Sequential assertions vs assertAll

Sequential assertions

  • Stops at first failure

    The test fails immediately when assertEquals finds a mismatch.

  • Fix one, run again

    You need multiple CI cycles to discover all the broken fields.

  • Simple and clear for one assertion

    When testing a single value, sequential is fine — assertAll would be over-engineering.

  • Natural guard for null checks

    assertNotNull(user) before user.getName() prevents NullPointerException.

assertAll

  • Runs all assertions, reports all failures

    MultipleFailuresError lists every mismatch — fix them all in one cycle.

  • One CI run reveals everything

    Especially valuable for API response validation with 5–10 fields.

  • Nested assertAll for structure

    Group status/headers/body separately. The nesting appears in the failure report.

  • No forget-to-flush trap

    Unlike TestNG SoftAssert, there is no separate final assertAll() call to forget.

⚠️ Common mistakes

  • Putting every assertion in assertAll regardless of dependencies. If user might be null, () -> assertEquals("Alice", user.getName()) inside assertAll will throw a NullPointerException, not an AssertionError. The NullPointerException is still caught and reported, but the message is less useful. Guard with assertNotNull(user) first, then use assertAll for the field assertions.
  • Forgetting to give assertAll a heading. The first argument is a string label for the failure report. assertAll(() -> assertEquals(...)) works (heading is optional), but assertAll("User fields", ...) makes the report immediately tell you which assertion group failed without reading the stack trace.
  • Nesting assertAll more than two levels deep. One level of nesting (outer: whole response, inner: body fields) is clean. Three or four levels becomes hard to read and diagnose. If you have that much to validate, consider splitting into multiple focused test methods.

🎯 Practice task

Practice finding all the failures at once. 20–25 minutes.

  1. Create a ProductDto class with fields String name, double price, String category, boolean inStock, int reviewCount.
  2. Write a ProductService.getById(int id) method that intentionally returns a ProductDto with at least two wrong field values (so your test will have multiple failures).
  3. Write ProductDtoTest.java with:
    • A sequential-assertions version of the test (standard assertEquals calls). Run it. Observe that it fails on the first wrong field and stops.
    • An assertAll version of the same test. Run it. Confirm the output shows both wrong fields simultaneously.
  4. Nested assertAll. Add a ProductResponse wrapper that holds a ProductDto and HTTP metadata (statusCode, contentType). Write a test with an outer assertAll for the HTTP metadata and a nested assertAll for the product fields.
  5. Stretch — null guard pattern. Make getById return null for an unknown id. Write a test that first calls assertNotNull(product), then inside assertAll asserts the fields. Confirm that when the product is null, you get a clear message about the null rather than an NPE inside assertAll.

Next lesson: @DisplayName and @Nested — making test reports readable and grouping related scenarios together.

// tip to track lessons you complete and pick up where you left off across devices.