AssertJ vs JUnit Assertions: Which Should You Use?

AssertJ vs JUnit Assertions: Which Should You Use?

JUnit 5 ships with a built-in assertion API. AssertJ is a third-party library that replaces it with a fluent, chainable alternative. Both work. The question is which produces better tests.

The Fundamental Difference

JUnit assertions are static methods with a fixed signature. AssertJ assertions use method chaining on a type-specific assertion object.

JUnit 5:

assertEquals("John", user.getName());
assertTrue(user.isActive());
assertNotNull(user.getId());

AssertJ:

assertThat(user.getName()).isEqualTo("John");
assertThat(user.isActive()).isTrue();
assertThat(user.getId()).isNotNull();

For single assertions the difference is mostly stylistic. The divergence grows with complexity.

Failure Messages

This is where AssertJ's advantage is most concrete.

String Comparison

JUnit 5 failure:

expected: <"John Doe"> but was: <"John  Doe">

AssertJ failure:

Expecting:
  "John  Doe"
to be equal to:
  "John Doe"
but was not.

Same information, but AssertJ's layout makes it faster to spot the double space.

Collection Comparison

List<String> actual = List.of("Alice", "Bob", "Charlie");
List<String> expected = List.of("Alice", "Charlie", "Dave");

JUnit 5:

expected: <[Alice, Charlie, Dave]> but was: <[Alice, Bob, Charlie]>

AssertJ:

Expecting ArrayList:
  ["Alice", "Bob", "Charlie"]
to contain exactly (and in same order):
  ["Alice", "Charlie", "Dave"]
but some elements were not found:
  ["Dave"]
and others were not expected:
  ["Bob"]

AssertJ tells you exactly what's missing and what's extra. With large collections, this eliminates manual diff work.

Chaining Multiple Assertions

JUnit 5 — one assertion per line, repeated subject:

assertNotNull(user);
assertEquals("John", user.getName());
assertEquals("john@example.com", user.getEmail());
assertTrue(user.isActive());
assertTrue(user.getAge() >= 18);

AssertJ — chained on the same subject:

assertThat(user)
    .isNotNull()
    .extracting(User::getName, User::getEmail, User::isActive)
    .containsExactly("John", "john@example.com", true);

assertThat(user.getAge()).isGreaterThanOrEqualTo(18);

The chained form is more concise and clearly groups related assertions.

Type-Safe Assertions

JUnit 5 assertions work on Object — you get no IDE help for type-specific methods:

// JUnit 5 — generic, no specific methods
assertTrue(list.contains("Alice"));    // works but reads backwards
assertTrue(list.size() > 0);          // manual size check

AssertJ returns a type-specific assertion object:

// AssertJ — IDE shows list-specific methods
assertThat(list)
    .contains("Alice")     // ListAssert.contains()
    .isNotEmpty()          // ListAssert.isNotEmpty()
    .hasSizeGreaterThan(0); // ListAssert.hasSizeGreaterThan()

When you write assertThat(myMap), you get MapAssert. assertThat(myPath) gives you PathAssert. The assertions are relevant to what you're testing.

Exception Handling

JUnit 5:

assertThrows(UserNotFoundException.class,
    () -> userService.findById(999L));

// With message check — requires capture
UserNotFoundException ex = assertThrows(UserNotFoundException.class,
    () -> userService.findById(999L));
assertEquals("User not found: 999", ex.getMessage());

AssertJ:

assertThatThrownBy(() -> userService.findById(999L))
    .isInstanceOf(UserNotFoundException.class)
    .hasMessage("User not found: 999")
    .hasMessageContaining("999");

AssertJ chains the exception assertions without requiring a separate capture step.

Soft Assertions

JUnit 5 stops at the first failure:

// If line 1 fails, lines 2-4 never run
assertEquals("John", user.getName());
assertEquals("john@example.com", user.getEmail());
assertTrue(user.isActive());
assertEquals(30, user.getAge());

JUnit 5 workaround with assertAll:

assertAll("user",
    () -> assertEquals("John", user.getName()),
    () -> assertEquals("john@example.com", user.getEmail()),
    () -> assertTrue(user.isActive()),
    () -> assertEquals(30, user.getAge())
);

AssertJ soft assertions:

SoftAssertions.assertSoftly(softly -> {
    softly.assertThat(user.getName()).isEqualTo("John");
    softly.assertThat(user.getEmail()).isEqualTo("john@example.com");
    softly.assertThat(user.isActive()).isTrue();
    softly.assertThat(user.getAge()).isEqualTo(30);
});

Both collect all failures. AssertJ's assertSoftly is more readable and gives better failure messages per assertion.

Comparison Table

Aspect JUnit 5 AssertJ
Dependency Built-in Extra dependency
Failure messages Basic Detailed, diff-focused
Method chaining No Yes
Type-specific assertions No Yes
Collection assertions Limited Comprehensive
Soft assertions assertAll SoftAssertions
Exception assertions assertThrows assertThatThrownBy
Custom conditions No Condition class
Learning curve Low Low–medium

When to Use JUnit 5 Built-ins

JUnit 5 assertions are fine when:

  • Simple projects — a handful of unit tests where adding a dependency isn't worth it
  • Existing codebase — tests are stable and passing; no reason to migrate
  • Team preference — the team knows JUnit assertions and isn't writing many complex assertions

When to Use AssertJ

Switch to AssertJ when:

  • Complex objects — you're asserting multiple fields, collections, or nested structures
  • Debugging test failures — detailed failure messages save time
  • Large test suites — consistent, readable assertions matter for maintenance
  • New projects — the setup cost is one line in pom.xml

For most projects doing real testing, AssertJ is the better long-term choice. The extra dependency is trivial, and the improvement in test readability and debugging speed compounds over time.

Read more

Start now free