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 ignoredassertAll 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 assertinguser.getName(). Ifuseris null, the body assertion throws aNullPointerException, not a meaningful assertion failure. Use individual assertions to guard preconditions,assertAllfor 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
assertAllregardless of dependencies. Ifusermight benull,() -> assertEquals("Alice", user.getName())insideassertAllwill throw aNullPointerException, not anAssertionError. TheNullPointerExceptionis still caught and reported, but the message is less useful. Guard withassertNotNull(user)first, then useassertAllfor the field assertions. - Forgetting to give
assertAlla heading. The first argument is a string label for the failure report.assertAll(() -> assertEquals(...))works (heading is optional), butassertAll("User fields", ...)makes the report immediately tell you which assertion group failed without reading the stack trace. - Nesting
assertAllmore 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.
- Create a
ProductDtoclass with fieldsString name,double price,String category,boolean inStock,int reviewCount. - Write a
ProductService.getById(int id)method that intentionally returns aProductDtowith at least two wrong field values (so your test will have multiple failures). - Write
ProductDtoTest.javawith:- A sequential-assertions version of the test (standard
assertEqualscalls). Run it. Observe that it fails on the first wrong field and stops. - An
assertAllversion of the same test. Run it. Confirm the output shows both wrong fields simultaneously.
- A sequential-assertions version of the test (standard
- Nested
assertAll. Add aProductResponsewrapper that holds aProductDtoand HTTP metadata (statusCode,contentType). Write a test with an outerassertAllfor the HTTP metadata and a nestedassertAllfor the product fields. - Stretch — null guard pattern. Make
getByIdreturnnullfor an unknown id. Write a test that first callsassertNotNull(product), then insideassertAllasserts the fields. Confirm that when the product is null, you get a clear message about the null rather than an NPE insideassertAll.
Next lesson: @DisplayName and @Nested — making test reports readable and grouping related scenarios together.