Overview
Standard JUnit5 assertions stop at the first failure. When verifying an object with multiple fields, you see one error and have to fix and rerun to find the next. AssertJ soft assertions collect all failures and report them together, giving you the full picture in a single test run.
When to Use
- Verifying an object with 5+ fields where any combination might fail
- Testing a response body with headers, status, and payload simultaneously
- Validating a data transformation where multiple output properties should hold
- You want faster debugging cycles — see all failures at once instead of one per run
When NOT to Use
- Single-field assertions — a standard
assertEqualsis simpler and clearer - Assertions that depend on each other (if A fails, B is meaningless) — use regular assertions
- Performance-critical test suites with thousands of assertions — soft assertions add overhead
Solution
Setup with Maven
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.0</version>
<scope>test</scope>
</dependency>
Setup with Gradle
testImplementation 'org.assertj:assertj-core:3.26.0'
Basic soft assertion
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
class UserTest {
@Test
void should_validate_all_user_fields() {
User user = userService.findById(1);
SoftAssertions softly = new SoftAssertions();
softly.assertThat(user.getId()).isEqualTo(1);
softly.assertThat(user.getEmail()).isEqualTo("alice@example.com");
softly.assertThat(user.getRole()).isEqualTo("admin");
softly.assertThat(user.isActive()).isTrue();
softly.assertThat(user.getCreatedAt()).isNotNull();
softly.assertAll();
}
}
Using assertSoftly lambda
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import org.junit.jupiter.api.Test;
class OrderTest {
@Test
void should_validate_order_response() {
OrderResponse response = orderService.placeOrder(request);
assertSoftly(softly -> {
softly.assertThat(response.getStatusCode()).isEqualTo(201);
softly.assertThat(response.getOrderId()).isNotNull();
softly.assertThat(response.getTotal()).isEqualByComparingTo("99.99");
softly.assertThat(response.getItems()).hasSize(3);
softly.assertThat(response.getEstimatedDelivery()).isAfterOrEqualTo(LocalDate.now().plusDays(1));
});
}
}
Soft assertions on collections
@Test
void should_validate_all_products() {
List<Product> products = productRepository.findAll();
SoftAssertions softly = new SoftAssertions();
softly.assertThat(products).hasSize(50);
for (Product p : products) {
softly.assertThat(p.getName()).isNotBlank();
softly.assertThat(p.getPrice()).isPositive();
softly.assertThat(p.getSku()).matches("^[A-Z]{3}-\\d{4}$");
}
softly.assertAll();
}
Custom assertion messages
@Test
void should_validate_with_custom_messages() {
Config config = configService.load("production");
SoftAssertions softly = new SoftAssertions();
softly.assertThat(config.getTimeout())
.as("Production timeout must be at least 30s")
.isGreaterThanOrEqualTo(30);
softly.assertThat(config.getRetries())
.as("Production retries must be between 1 and 5")
.isBetween(1, 5);
softly.assertThat(config.getFeatureFlags())
.as("Feature flags must include 'monitoring'")
.containsKey("monitoring");
softly.assertAll();
}
Soft assertions with AssertJ object assertions
@Test
void should_validate_user_dto() {
UserDto user = UserDto.builder()
.id(42)
.name("Alice")
.email("alice@example.com")
.role("admin")
.active(true)
.build();
assertThat(user)
.usingRecursiveComparison()
.ignoringFields("createdAt")
.isEqualTo(expectedUser);
}
Variants
JUnit5 assertAll (built-in)
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Test
void should_validate_with_junit5_assert_all() {
User user = userService.findById(1);
assertAll(
() -> assertEquals(1, user.getId()),
() -> assertEquals("alice@example.com", user.getEmail()),
() -> assertTrue(user.isActive()),
() -> assertEquals("admin", user.getRole())
);
}
Soft assertions with @RegisterExtension
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class ServiceTest {
@RegisterExtension
final SoftAssertionsExtension soft = new SoftAssertionsExtension();
@Test
void should_validate(ServiceSoftAssertions softly) {
Result result = service.execute();
softly.assertThat(result.getCode()).isEqualTo(200);
softly.assertThat(result.getData()).isNotEmpty();
}
}
Best Practices
-
For a deeper guide, see Java Testcontainers Integration Tests.
-
Call
softly.assertAll()at the end — without it, failures are silently swallowed -
Use
assertSoftlylambda for cleaner syntax when you don’t need to reuse theSoftAssertionsobject -
Add custom messages with
.as()for non-obvious assertions — they appear in failure output -
Don’t mix soft and hard assertions in the same test — the hard assertion stops early, defeating the purpose
-
Keep soft assertion blocks focused on one logical object or response
Common Mistakes
- Forgetting
assertAll(): soft assertions withoutassertAll()always pass, even when assertions fail. - Using soft assertions for independent tests: each test should verify one behavior. Soft assertions are for multiple checks on the same logical unit.
- Overusing soft assertions for simple checks: if you have 2 assertions, regular
assertEqualsis fine. Soft assertions shine at 5+ checks. - Not adding descriptive messages: when 10 assertions fail, you need context to know which is which.
Troubleshooting
- Flaky tests: isolate shared state, time, and randomness. Make tests independent and deterministic; quarantine persistently flaky tests.
- High coverage but bugs in production: coverage does not guarantee correctness. Add mutation testing, property-based tests, or contract tests.
- Slow test suite: parallelize, mock slow dependencies, and avoid end-to-end tests for logic that can be unit tested.
- Tests pass locally but fail in CI: check environment differences, timezone, locale, and dependency versions. Pin tool versions.
- Debugging a failing integration test: log request/response payloads and use a dedicated test database. Reset state before each test.
Further Reading
- Official documentation: check the current reference for the framework or tool used.
- Related guides: explore the testing and java guides for deeper coverage.
- Complementary patterns: review design patterns applicable to your technology stack.
- Public postmortems: study real incidents from teams that faced similar production issues.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply junit5 soft assertions with assertj when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Common Production Pitfalls
- Copying the example without adapting it to real data volumes and failure modes.
- Skipping load and error-injection tests before the first production deployment.
- Hard-coding values that should be configurable per environment.
- Forgetting to add logging and monitoring at each step.
- Deploying without a rollback plan or a tested backup strategy.
- Assuming the minimal example will scale without adding caching or batching.
- Not documenting the version and configuration used in production.
- Letting the recipe sit unchanged when dependencies or scale evolve.
Frequently Asked Questions
What is the difference between AssertJ soft assertions and JUnit5 assertAll?
Both collect multiple failures. assertAll uses standard JUnit5 assertions. AssertJ soft assertions give you fluent assertion methods (assertThat, isNotNull, matches) which are more readable and type-safe.
Can I use soft assertions with custom assertions?
Yes. Create a custom AssertJ assertion class extending AbstractAssert, then use it inside a SoftAssertions block:
softly.assertThat(user).hasValidEmail().hasActiveSubscription();
Do soft assertions work with exceptions?
No. Soft assertions collect assertion failures, not exceptions. If code throws an exception, the test fails immediately. Use assertThatThrownBy for exception testing.
How do I see all soft assertion failures in CI?
AssertJ prints all failures to standard output when assertAll() is called. In CI, check the test report — the failure message lists every failed assertion with its custom description.
How do I use soft assertions with parameterized tests?
Wrap each parameterized test invocation in its own SoftAssertions block. Do not share a SoftAssertions instance across parameter invocations — it accumulates failures from all runs. Use @ParameterizedTest with @MethodSource and assert within each invocation. If a single parameter fails, only that invocation fails, not the entire test.
How do I combine soft assertions with Hamcrest matchers?
Use assertThat(actual, matcher) inside a SoftAssertions block. AssertJ's SoftAssertions supports Hamcrest matchers via assertThat overloads. Alternatively, use MatcherAssert.assertThat wrapped in a try-catch that collects AssertionError instances. This lets you migrate from Hamcrest to AssertJ gradually without rewriting all assertions at once.
What is the performance impact of soft assertions?
Soft assertions add minimal overhead — each assertion is evaluated and stored in a list. The assertAll() call iterates the list and throws if any failed. For 10-20 assertions per test, the overhead is negligible. Avoid soft assertions in tight loops with thousands of iterations — use a single aggregate assertion instead (e.g., collect results into a list and assert once).
How do I group soft assertions by logical section?
Use multiple SoftAssertions blocks per test, one per logical section (e.g., one for field validation, one for computed properties). Call assertAll() after each block so failures from the first section are reported before the second runs. This gives clearer test output and helps identify which logical section failed. Alternatively, use SoftAssertionsProvider with assertSoftly for a scoped block.
Can I use soft assertions with Kotlin?
Yes. AssertJ works in Kotlin but consider using assertk or Kluent for more idiomatic Kotlin syntax. With AssertJ in Kotlin, use SoftAssertions().apply { softly -> softly.assertThat(x).isEqualTo(y) }.assertAll(). For Kotlin-specific soft assertions, assertk provides assertAll { assert(x).isEqualTo(y) } with a cleaner DSL.
How do I migrate from JUnit4 Assert to JUnit5 soft assertions?
Replace org.junit.Assert.assertEquals calls with AssertJ assertThat inside a SoftAssertions block. Add AssertJ dependency (org.assertj:assertj-core:3.25+). Replace Assert.assertEquals(expected, actual) with softly.assertThat(actual).isEqualTo(expected). Add softly.assertAll() at the end. The migration is mechanical — no test logic changes needed. Run both old and new assertions side by side during migration to catch regressions.
Related Resources
Unit Testing
How to write fast, deterministic unit tests with mocks and assertions in Python, JavaScript, and Java.
RecipeJava Testcontainers Integration Tests
How to use Testcontainers in JUnit5 to spin up real Postgres, Redis, and Kafka containers for integration tests that are reliable and reproducible.
RecipeStub External HTTP APIs with WireMock in Java
Use WireMock in Java tests to stub external HTTP services. Covers response templating, delay simulation, stateful stubs, and request verification.
GuideJUnit 5: Extensions, Parameterized Tests, Dynamic Tests
Master JUnit 5 for modern Java testing: extensions model, parameterized tests, dynamic tests, test interfaces, lifecycle, conditional execution, and JUnit Platform integration.