Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Soft assertions and more AssertJ checks

An ordinary assertion is hard: the first failed check throws an exception, and the rest of the test does not run. If a post has a wrong title and a wrong author, you learn about the title, fix it, run again — and only then see the author. A soft assertion does not stop the test: it remembers the failure and goes on, and at the end reports all failures at once. In JUnit this is done by assertAll; AssertJ has its own version with the usual assertThat chains.

The tests of this lesson live in src/test/java/com/example/blog/PostSoftAssertionsTest.java; AssertJ is already in pom.xml since the lesson about it.

assertSoftly

// src/test/java/com/example/blog/PostSoftAssertionsTest.java
package com.example.blog;
 
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
 
import java.util.List;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
 
class PostSoftAssertionsTest {
 
    private final InMemoryPostRepository repository = new InMemoryPostRepository();
    private final PostService service = new PostService(repository, (to, message) -> { });
 
    @Test
    void publishKeepsFields() {
        Post post = service.publish(service.create("Hello", "anna").id());
 
        assertSoftly(softly -> {
            softly.assertThat(post.id()).isEqualTo(1L);
            softly.assertThat(post.title()).isEqualTo("Hello");
            softly.assertThat(post.author()).isEqualTo("anna");
            softly.assertThat(post.published()).isTrue();
        });
    }
}

Inside the lambda the checks go through the softly object instead of the static assertThat. assertSoftly runs the whole block and only then fails if something was wrong. If the test expected the title "Hi" and the author "oleg", the report lists both, each with its line:

org.assertj.core.error.AssertJMultipleFailuresError:
 
Multiple Failures (2 failures)
-- failure 1 --
expected: "Hi"
 but was: "Hello"
at SoftFailTest.lambda$publishKeepsFields$0(SoftFailTest.java:18)
-- failure 2 --
expected: "oleg"
 but was: "anna"
at SoftFailTest.lambda$publishKeepsFields$0(SoftFailTest.java:19)

There is also a form without a lambda — create the object yourself and call assertAll() at the end:

// src/test/java/com/example/blog/PostSoftAssertionsTest.java
@Test
void softAssertionsObject() {
    Post post = service.create("Hello", "anna");
 
    SoftAssertions softly = new SoftAssertions();
    softly.assertThat(post.title()).startsWith("Hel");
    softly.assertThat(post.published()).isFalse();
    softly.assertAll();
}

assertSoftly or assertAll

JUnit assertAllAssertJ assertSoftly
Checks insideSeparate lambdas, one per checkOrdinary statements, any number
AssertionsassertEquals, assertTrue and othersAll AssertJ chains
Extra libraryNoAssertJ

If the project already uses AssertJ, assertSoftly reads better; in plain JUnit tests assertAll is enough. Use soft assertions for several fields of one result, not to glue unrelated checks into one test.

A group of checks on one object: satisfies

// src/test/java/com/example/blog/PostSoftAssertionsTest.java
@Test
void satisfies() {
    Post post = service.create("  Hello ", "anna");
 
    assertThat(post).satisfies(p -> {
        assertThat(p.title()).isEqualTo("Hello");
        assertThat(p.published()).isFalse();
    });
}
 
@Test
void allSatisfy() {
    service.create("First", "anna");
    service.create("Second", "oleg");
 
    List<Post> posts = repository.findAll();
 
    assertThat(posts).allSatisfy(p -> assertThat(p.published()).isFalse());
    assertThat(posts).anySatisfy(p -> assertThat(p.author()).isEqualTo("oleg"));
}

satisfies runs a block of checks on the value — handy in the middle of a chain. allSatisfy checks every element of a list, anySatisfy — that at least one element passes. Both tests pass. (containsExactly, extracting and filteredOn were shown in the AssertJ lesson.)

Comparing field by field: usingRecursiveComparison

The id is assigned by the repository, so isEqualTo(new Post(...)) cannot be used directly. The recursive comparison checks the fields one by one and can skip some of them:

// src/test/java/com/example/blog/PostSoftAssertionsTest.java
@Test
void recursiveComparison() {
    Post post = service.create("Hello", "anna");
 
    assertThat(post)
            .usingRecursiveComparison()
            .ignoringFields("id")
            .isEqualTo(new Post(null, "Hello", "anna", false));
}

On a failure it lists every differing field (shortened):

Expecting actual:
  Post[id=1, title=Hello, author=anna, published=false]
to be equal to:
  Post[id=null, title=Hello, author=oleg, published=true]
when recursively comparing field by field, but found the following 2 differences:
 
field/property 'author' differ:
- actual value  : "anna"
- expected value: "oleg"
...
field/property 'published' differ:
- actual value  : false
- expected value: true

It also works for objects of different classes with the same field names — for example, a post and a DTO.

Common mistake

With the new SoftAssertions() form, assertAll() at the end is forgotten:

SoftAssertions softly = new SoftAssertions();
softly.assertThat(post.title()).isEqualTo("Hi");
softly.assertThat(post.published()).isTrue();

Both checks are wrong, but the test is green:

[INFO] Running com.example.blog.NoAssertAllTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.858 s -- in com.example.blog.NoAssertAllTest

Soft assertions only collect failures; assertAll() reports them. Prefer assertSoftly(softly -> ...) — it calls assertAll() for you.

Thanks for reading!

Donate to the site — we work hard for you.