assertj
Reference for AssertJ - the canonical JVM fluent-assertion library pairable with JUnit 5 / TestNG / Spock; covers the assertThat() entry point, collection matchers (contains, containsExactly, allSatisfy, extracting), exception assertions (assertThatThrownBy, catchThrowable), SoftAssertions for multi-failure collection, recursive comparison (usingRecursiveComparison) for deep equality, and domain-specific custom assertions via AbstractAssert. Use when writing JVM tests that need richer failure messages than built-in assertEquals, or when verifying complex object graphs, exception types, or collections.
assertj
Overview
AssertJ is "a Java library that provides a rich set of assertions and truly helpful error messages, improves test code readability, and is designed to be super easy to use within your favorite IDE." It ships modules for JDK core types, Guava, Joda-Time, and databases; this skill covers assertj-core (JDK types).
Works alongside junit5-tests, testng-tests, or spock-tests. AssertJ handles the assertion layer; those skills handle the test runner.
This skill is a reference - defines the matcher catalog and patterns; does not run tests.
When to use
Step 1 - Install
Maven:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>Gradle:
testImplementation("org.assertj:assertj-core:3.27.7")Static import the entry class once per test file:
import static org.assertj.core.api.Assertions.*;Step 2 - assertThat entry point
Per assertj.github.io/doc/#basic-assertions:
assertThat(actual) returns a type-specific assertion object. All assertions chain fluently from it.
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat("text").isNotNull()
.startsWith("te")
.contains("ex");Common predicates available on every assertion:
assertThat(value).isEqualTo(expected);
assertThat(value).isNotEqualTo(other);
assertThat(value).isNull();
assertThat(value).isNotNull();
assertThat(value).isSameAs(ref); // reference equality
assertThat(value).isInstanceOf(MyClass.class);
assertThat(flag).isTrue();
assertThat(flag).isFalse();Use .as("description") to label an assertion in failure output:
assertThat(user.getAge()).as("user age").isGreaterThan(0);Step 3 - Collection assertions
Per assertj.github.io/doc/#collection-assertions:
assertThat(list).hasSize(9);
assertThat(list).isEmpty();
assertThat(list).contains(frodo, sam); // any order, subset
assertThat(list).containsExactly(frodo, sam, pippin); // exact order, exact set
assertThat(list).containsOnly(frodo, sam); // any order, exact set
assertThat(list).doesNotContain(sauron);Element-level verification:
assertThat(hobbits).allSatisfy(c -> {
assertThat(c.getRace()).isEqualTo(HOBBIT);
assertThat(c.getName()).isNotEqualTo("Sauron");
});
assertThat(hobbits).anySatisfy(c ->
assertThat(c.getName()).isEqualTo("Sam"));Extraction and filtering:
// Extract a property then assert on extracted values
assertThat(fellowship).extracting("name")
.contains("Boromir", "Gandalf", "Frodo");
// Extract multiple properties as tuples
assertThat(fellowship).extracting("name", "age")
.contains(tuple("Boromir", 37), tuple("Sam", 38));
// Filter before asserting
assertThat(fellowship).filteredOn(c -> c.getName().contains("o"))
.containsOnly(aragorn, frodo);Step 4 - Exception assertions
Per assertj.github.io/doc/#exception-assertions:
Primary form - assertThatThrownBy:
assertThatThrownBy(() -> parser.parse(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("null input");Type-first form - assertThatExceptionOfType:
assertThatExceptionOfType(IOException.class)
.isThrownBy(() -> { throw new IOException("boom!"); })
.withMessage("%s!", "boom")
.withNoCause();BDD form - catchThrowable: separates the WHEN step from THEN:
// WHEN
Throwable thrown = catchThrowable(() -> names[9]);
// THEN
assertThat(thrown).isInstanceOf(ArrayIndexOutOfBoundsException.class)
.hasMessageContaining("9");Typed capture - catchThrowableOfType: returns the concrete exception for further assertion:
TextException ex = catchThrowableOfType(TextException.class,
() -> { throw new TextException("boom!", 1, 5); });
assertThat(ex.line).isEqualTo(1);Cause chain inspection:
assertThat(thrown).hasCauseInstanceOf(NullPointerException.class);
assertThat(thrown).hasRootCauseInstanceOf(SocketException.class);
assertThat(thrown).cause().hasMessage("underlying cause");Assert no exception:
assertThatCode(() -> service.process(input)).doesNotThrowAnyException();Step 5 - SoftAssertions
Per assertj.github.io/doc/#soft-assertions:
SoftAssertions collect failures instead of stopping at the first one. All violations are reported together in a single error.
Instance form:
SoftAssertions softly = new SoftAssertions();
softly.assertThat(actual.getName()).isEqualTo("Frodo");
softly.assertThat(actual.getAge()).isEqualTo(33);
softly.assertThat(actual.getRace()).isEqualTo(HOBBIT);
softly.assertAll(); // throws one error listing all failuresStatic helper - assertSoftly: manages lifecycle automatically; assertAll() is called on exit:
assertSoftly(softly -> {
softly.assertThat(frodo.getName()).isEqualTo("Frodo");
softly.assertThat(frodo.getAge()).isEqualTo(33);
softly.assertThat(frodo.getRace()).isEqualTo(HOBBIT);
});Use SoftAssertions when a test covers multiple independent properties of the same subject, so a single run reveals all mismatches rather than stopping at the first.
Step 6 - Recursive comparison
Per assertj.github.io/doc/#recursive-comparison:
usingRecursiveComparison() compares object graphs field-by-field without requiring equals overrides.
assertThat(sherlock).usingRecursiveComparison()
.isEqualTo(sherlockClone);Exclude fields:
assertThat(actual).usingRecursiveComparison()
.ignoringFields("id", "home.address.street")
.isEqualTo(expected);Exclude by regex pattern:
assertThat(actual).usingRecursiveComparison()
.ignoringFieldsMatchingRegexes(".*At", ".*Id")
.isEqualTo(expected);Ignore nulls in actual:
assertThat(partial).usingRecursiveComparison()
.ignoringActualNullFields()
.isEqualTo(expected);Custom comparator per type:
BiPredicate<Double, Double> closeEnough = (d1, d2) -> Math.abs(d1 - d2) <= 0.5;
assertThat(frodo).usingRecursiveComparison()
.withEqualsForType(closeEnough, Double.class)
.isEqualTo(tallerFrodo);Strict type checking:
assertThat(actual).usingRecursiveComparison()
.withStrictTypeChecking()
.isEqualTo(expected);Step 7 - Custom assertions
Per assertj.github.io/doc/#custom-assertions:
Extend AbstractAssert to create domain-specific assertion classes. The type parameters are <SELF, ACTUAL> where SELF is the concrete assertion class (for chaining):
public class PersonAssert extends AbstractAssert<PersonAssert, Person> {
public PersonAssert(Person actual) {
super(actual, PersonAssert.class);
}
public PersonAssert hasName(String name) {
isNotNull();
if (!actual.getName().equals(name)) {
failWithMessage("Expected name <%s> but was <%s>",
name, actual.getName());
}
return this;
}
public PersonAssert isAdult() {
if (actual.getAge() < 18) {
failWithMessage("Expected person to be an adult");
}
return this;
}
}Expose it via a static factory that mirrors assertThat:
public static PersonAssert assertThat(Person actual) {
return new PersonAssert(actual);
}Usage then reads like built-in assertions:
assertThat(person).hasName("Alice").isAdult();Example - full test method
@Test
void order_ships_to_correct_address() {
Order order = orderService.place(items, address);
assertThat(order).isNotNull();
assertThat(order.getItems()).hasSize(2)
.extracting("sku")
.containsExactly("ITEM-001", "ITEM-002");
assertThat(order).usingRecursiveComparison()
.ignoringFields("id", "createdAt")
.isEqualTo(expectedOrder);
}
@Test
void invalid_quantity_throws() {
assertThatThrownBy(() -> orderService.place(emptyItems, address))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("at least one item");
}
@Test
void order_summary_fields_all_valid() {
OrderSummary summary = orderService.summarize(orderId);
assertSoftly(softly -> {
softly.assertThat(summary.getTotal()).isGreaterThan(BigDecimal.ZERO);
softly.assertThat(summary.getItemCount()).isEqualTo(2);
softly.assertThat(summary.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
});
}Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Mix assertEquals and assertThat styles in same suite | Reader confusion; inconsistent failure messages | Pick one style and lint-enforce it |
usingRecursiveComparison without ignoringFields for volatile fields | Brittle: timestamps and generated IDs differ on every run | Exclude with ignoringFieldsMatchingRegexes(".*At", ".*Id") |
Skip assertAll() when using SoftAssertions instance | Failures are silently swallowed | Use assertSoftly() helper or always call assertAll() in a try-finally |
assertThat(flag).isEqualTo(true) instead of isTrue() | Loses semantic clarity in failure messages | Use isTrue() / isFalse() |
| Skip message check on exception assertions | Test passes for any exception of that type | Always add .hasMessageContaining(...) |