AssertJ: Fluent Assertions, Soft Assertions & Custom Assertions

AssertJ provides a fluent, chainable assertion API for Java tests. It produces far more readable failure messages than JUnit's built-in assertions, supports collection assertions, exception assertions, field-by-field comparisons, and lets you write domain-specific custom assertion classes.

Share

Contents

Dependency

AssertJ Core is included in spring-boot-starter-test. For standalone projects:

XML
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.26.0</version>
  <scope>test</scope>
</dependency>

All assertions start with a static import: import static org.assertj.core.api.Assertions.*;

Basic Assertions

The class below shows the implementation. Key points are highlighted in the inline comments.

Java
import static org.assertj.core.api.Assertions.*;

// Equality
assertThat(42).isEqualTo(42);
assertThat("hello").isNotEqualTo("world");

// Null checks
assertThat(user).isNotNull();
assertThat(result).isNull();

// Boolean
assertThat(list.isEmpty()).isTrue();
assertThat(service.isActive()).isFalse();

// Type check
assertThat(obj).isInstanceOf(String.class);

// Comparable
assertThat(score).isGreaterThan(0)
                 .isLessThanOrEqualTo(100)
                 .isBetween(1, 99);

// Same reference
assertThat(actual).isSameAs(expected);

String Assertions

The class below shows the implementation. Key points are highlighted in the inline comments.

Java
String email = "alice@example.com";

assertThat(email)
    .isNotEmpty()
    .isNotBlank()
    .startsWith("alice")
    .endsWith(".com")
    .contains("@")
    .doesNotContain("bob")
    .matches("^[\\w.]+@[\\w.]+\\.[a-z]{2,}$")
    .hasSize(17)
    .isEqualToIgnoringCase("ALICE@EXAMPLE.COM");

Collection Assertions

The class below shows the implementation. Key points are highlighted in the inline comments.

Java
List<String> fruits = List.of("apple", "banana", "cherry");

assertThat(fruits)
    .hasSize(3)
    .isNotEmpty()
    .contains("banana")
    .containsExactly("apple", "banana", "cherry")    // exact order
    .containsExactlyInAnyOrder("cherry", "apple", "banana")
    .doesNotContain("durian")
    .allMatch(f -> f.length() > 3)
    .anyMatch(f -> f.startsWith("a"))
    .noneMatch(f -> f.isBlank());

// Extracting fields from a list of objects
List<User> users = List.of(
    new User(1L, "Alice", "admin"),
    new User(2L, "Bob",   "user")
);

assertThat(users)
    .extracting(User::name)
    .containsExactlyInAnyOrder("Alice", "Bob");

assertThat(users)
    .extracting("name", "role")
    .containsExactly(
        tuple("Alice", "admin"),
        tuple("Bob",   "user")
    );

// Map assertions
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 80);

assertThat(scores)
    .containsKey("Alice")
    .containsEntry("Bob", 80)
    .doesNotContainKey("Charlie")
    .hasSize(2);

Object & Field Assertions

Use usingRecursiveComparison() for deep field-by-field equality without requiring equals(), and extracting() to pluck specific fields for assertion.

Java
Order expected = new Order("o1", "Widget", 3, OrderStatus.PENDING);
Order actual   = orderService.place("Widget", 3);

// Deep recursive comparison — ignores equals()
assertThat(actual)
    .usingRecursiveComparison()
    .ignoringFields("id", "createdAt")   // skip generated fields
    .isEqualTo(expected);

// Extracting specific fields
assertThat(actual)
    .extracting(Order::product, Order::quantity)
    .containsExactly("Widget", 3);

// Conditions
assertThat(actual.status()).satisfies(status -> {
    assertThat(status).isNotNull();
    assertThat(status.name()).isIn("PENDING", "PROCESSING");
});

Exception Assertions

The class below shows the implementation. Key points are highlighted in the inline comments.

Java
// Assert type and message
assertThatThrownBy(() -> userService.findById(-1L))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("must be positive")
    .hasMessageStartingWith("User ID");

// Assert a specific exception type with typed result
assertThatExceptionOfType(EntityNotFoundException.class)
    .isThrownBy(() -> userService.findById(999L))
    .withMessage("User 999 not found")
    .withNoCause();

// Assert no exception is thrown
assertThatNoException()
    .isThrownBy(() -> userService.findById(1L));

// assertThatCode is another common form
assertThatCode(() -> configService.reload())
    .doesNotThrowAnyException();

Soft Assertions

Normal assertions stop at the first failure. SoftAssertions collects all failures and reports them together: useful when you need to validate a full object in one test pass.

Java
import org.assertj.core.api.SoftAssertions;

@Test
void validatesUserProfile() {
    User user = userService.loadProfile(1L);

    SoftAssertions softly = new SoftAssertions();
    softly.assertThat(user.name()).isEqualTo("Alice");
    softly.assertThat(user.email()).contains("@");
    softly.assertThat(user.age()).isBetween(18, 120);
    softly.assertThat(user.role()).isIn("admin", "user", "viewer");
    softly.assertAll();  // throws AssertionError listing ALL failures
}

// Alternatively use the lambda form which calls assertAll automatically
@Test
void validatesOrderSoftly() {
    Order order = orderService.place("Widget", 5);

    SoftAssertions.assertSoftly(softly -> {
        softly.assertThat(order.id()).isNotNull();
        softly.assertThat(order.product()).isEqualTo("Widget");
        softly.assertThat(order.quantity()).isEqualTo(5);
        softly.assertThat(order.status()).isEqualTo(OrderStatus.PENDING);
    });
}

Custom Assertions

Extend AbstractAssert to create domain-specific assertion classes that read naturally and produce clear failure messages.

Java
import org.assertj.core.api.AbstractAssert;

public class OrderAssert extends AbstractAssert<OrderAssert, Order> {

    public OrderAssert(Order order) {
        super(order, OrderAssert.class);
    }

    public static OrderAssert assertThat(Order order) {
        return new OrderAssert(order);
    }

    public OrderAssert hasProduct(String product) {
        isNotNull();
        if (!actual.product().equals(product)) {
            failWithMessage("Expected order product to be <%s> but was <%s>",
                product, actual.product());
        }
        return this;
    }

    public OrderAssert isPending() {
        isNotNull();
        if (actual.status() != OrderStatus.PENDING) {
            failWithMessage("Expected order to be PENDING but was <%s>", actual.status());
        }
        return this;
    }

    public OrderAssert hasQuantityGreaterThan(int min) {
        isNotNull();
        if (actual.quantity() <= min) {
            failWithMessage("Expected quantity > %d but was %d", min, actual.quantity());
        }
        return this;
    }
}
Java
// Usage — reads like plain English
Order order = orderService.place("Gadget", 3);

OrderAssert.assertThat(order)
    .hasProduct("Gadget")
    .isPending()
    .hasQuantityGreaterThan(0);