Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 55

Fluent Assertions with AssertJ

Recommended resources

Hello! Welcome to the eighth module of our course, where we'll focus on Testing Spring Applications.

In the previous modules, we've built a functional and secure REST API. We've handled everything from creating controllers and services to integrating a database and securing our endpoints. But how can we be confident that our code works as expected and continues to work as we add new features or refactor existing ones? The answer is automated testing.

This module will equip you with the skills to write effective and maintainable tests for your Spring Boot applications. We'll start with the very foundation of any good test: the assertion.

Today's lesson focuses on making your tests more readable and expressive. Our learning outcome is to use AssertJ for fluent assertions in tests. We'll explore why this library has become the standard in the Java community and how it helps you write tests that are not only powerful but also easy to understand.

1. Why Do We Need Better Assertions?

If you've written tests in Java before, you might be familiar with JUnit's built-in assertions like assertEquals(), assertTrue(), and assertNotNull(). While functional, they have a few common drawbacks that can make tests harder to read and debug.

Let's look at a classic example with assertEquals():

JUnit vs. AssertJ Assertion Syntax Comparison
This image highlights a common mistake with JUnit's `assertEquals(expected, actual)`. It's easy to accidentally swap the arguments. When the test fails, the error message becomes confusing ("expected: <actual> but was: <expected>"). AssertJ's fluent `assertThat(actual).isEqualTo(expected)` syntax makes this kind of mistake impossible and is more natural to read.

Another issue is the lack of descriptive failure messages. Consider this assertion:

// Using standard JUnit
List<String> results = findResults();
assertTrue(results.contains("expectedValue"));

If this test fails, the error message will be something like:
java.lang.AssertionError: expected: <true> but was: <false>

This tells you that the condition failed, but it gives you no information about the actual content of the results list. You don't know if the list was empty or if it contained other values. To debug this, you'd have to run the test again with a debugger or add print statements.

AssertJ solves these problems by providing:

  • A Fluent API: Assertions are written in a natural, chainable way that reads like a sentence.
  • Rich, Descriptive Error Messages: When a test fails, AssertJ tells you exactly what was expected and what the actual value was, saving you significant debugging time.
  • Discoverability: Thanks to its design, your IDE's autocomplete feature becomes a powerful tool for discovering all the possible assertions you can make on a given object.

To see just how significant these improvements are, it's helpful to understand the limitations of older testing libraries.

Better Assertions with AssertJ by Tim te Beek @ Spring I/O 2025

This segment from a conference talk by Tim te Beek provides an excellent tour of the anti-patterns and unhelpful error messages that were common before libraries like AssertJ became popular.

Watch the section from 06:12 to 21:57. Pay close attention to the examples of failing tests and the vague error messages they produce. This will give you a strong appreciation for the clarity that AssertJ brings to testing.


2. Getting Started with AssertJ

Using AssertJ in your project is straightforward. Since you're building a Spring Boot application with Maven, the spring-boot-starter-test dependency already includes AssertJ for you, so there's no need to add anything to your pom.xml.

To start writing assertions, you just need a single static import in your test class:

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

This import gives you access to the crucial assertThat() method, which is the entry point for all AssertJ assertions. The basic structure is always the same:

assertThat(theActualValue).is...()

You pass the object or value you want to test to assertThat(), and then chain one or more assertion methods to it.

Introduction to AssertJ

Let's review the fundamental setup and syntax. The Baeldung article 'Introduction to AssertJ' provides a clear, concise starting point.

Please read sections 3 ('Introduction'), 4.1 ('Getting Started'), and 4.2 ('Writing Assertions'). Focus on the fundamental assertThat() syntax and the concept of chaining assertion methods to form a fluent expression.


3. Core Assertions in Action

The real power of AssertJ lies in its rich set of type-specific assertions. Let's explore some of the most common ones.

General Object and String Assertions

// Let's assume we have a User object
User user = new User("admin");
User anotherUser = new User("admin");

// Object assertions
assertThat(user).isNotNull();
assertThat(user).isInstanceOf(User.class);
assertThat(user).isNotEqualTo(anotherUser); // Compares using .equals(). Assumes not implemented or different objects.

// String assertions
String name = user.getName();
assertThat(name).isEqualTo("admin");
assertThat(name).startsWith("ad").endsWith("in");
assertThat(name).contains("dmi");
assertThat(name).isEqualToIgnoringCase("ADMIN");

Collection Assertions

This is where AssertJ truly shines compared to standard JUnit.

List<String> fellowship = List.of("Frodo", "Sam", "Pippin");

assertThat(fellowship).hasSize(3);
assertThat(fellowship).isNotEmpty();
assertThat(fellowship).contains("Sam", "Frodo");
assertThat(fellowship).doesNotContain("Sauron");

// Check for exact contents in any order
assertThat(fellowship).containsExactlyInAnyOrder("Sam", "Pippin", "Frodo");

// A very powerful feature: extracting properties for assertion
List<User> users = List.of(new User("Frodo"), new User("Sam"));
assertThat(users)
    .extracting(User::getName) // Extracts the 'name' property from each User
    .contains("Frodo", "Sam")
    .doesNotContain("Gollum");

Map Assertions

Map<String, String> ringBearers = Map.of("The One Ring", "Frodo", "Nenya", "Galadriel");

assertThat(ringBearers).hasSize(2);
assertThat(ringBearers).containsKey("The One Ring");
assertThat(ringBearers).containsValue("Frodo");
assertThat(ringBearers).containsEntry("Nenya", "Galadriel");
assertThat(ringBearers).doesNotContainKey("Vilya");

Exception Assertions

Testing for exceptions with AssertJ is incredibly clean and readable.

Mastering Java Testing With JUnit, Mockito, and AssertJ in IntelliJ IDEA

AssertJ provides a particularly elegant way to test for exceptions. This short clip from a talk by Ken Kousen demonstrates how readable and powerful AssertJ's exception assertions are.

Watch from 46:42 to 47:43. Notice the chain of assertions: assertThatExceptionOfType(...) followed by .isThrownBy(...), .withCauseExactlyInstanceOf(...), and .withMessageContaining(...). This allows you to test multiple aspects of a thrown exception in one statement.

Here's how you'd write it:

// Suppose this method throws an exception
public void placeRingOnFinger() {
    throw new RuntimeException("The Eye of Sauron is watching!");
}

@Test
void whenPlacingRing_thenSauronNotices() {
    assertThatThrownBy(() -> placeRingOnFinger())
        .isInstanceOf(RuntimeException.class)
        .hasMessageContaining("Sauron");
}

4. Improving Your Test Reports

Custom Error Messages with as()

You can add context to your assertions using the as() method. This is invaluable when a test fails, as it explains the intent of the assertion.

User user = new User("admin", 0); // name, postCount

assertThat(user.getPostCount())
    .as("A newly created user should have zero posts")
    .isEqualTo(0);

If this fails because getPostCount() returns 1, the error message will be much clearer:
[A newly created user should have zero posts] expected: <0> but was: <1>

Checking Multiple Assertions with Soft Assertions

Normally, a test method stops at the first failed assertion. Sometimes, you want to check several properties of an object and see all the failures at once. AssertJ's "soft assertions" make this possible.

Better Assertions with AssertJ by Tim te Beek @ Spring I/O 2025

This clip from Tim te Beek's talk explains the concept of 'soft assertions' and how they help you get a complete picture of all failed checks in a single test run.

Watch from 32:47 to 34:41. This is a more advanced technique, but it's very useful for testing complex Data Transfer Objects (DTOs) or domain models, so it's good to be aware of it.

Here's a quick example of how you might use it:

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

@Test
void userPropertiesAreSetCorrectly() {
    User frodo = new User("Frod", 33); // Typo in name
    frodo.setRace("Hobbi"); // Typo in race

    SoftAssertions.assertSoftly(softly -> {
        softly.assertThat(frodo.getName()).as("name").isEqualTo("Frodo");
        softly.assertThat(frodo.getAge()).as("age").isEqualTo(33);
        softly.assertThat(frodo.getRace()).as("race").isEqualTo("Hobbit");
    });
}

When this test runs, it won't just fail on the name. It will report both the name and race failures together.

Test your understanding!

Let's imagine you are testing a method getTopSellingProducts() which returns a List<Product>. You want to verify that the list contains exactly three products: a "Laptop", a "Mouse", and a "Keyboard", in any order.

Which AssertJ assertion is the most precise and appropriate for this specific requirement?

  1. assertThat(products).hasSize(3);
  2. assertThat(products).contains("Laptop", "Mouse", "Keyboard");
  3. assertThat(products).containsExactlyInAnyOrder("Laptop", "Mouse", "Keyboard");

Think about what would happen if the method returned ["Laptop", "Mouse", "Mouse"]. How would each assertion behave?

Show answer

The correct answer is 3. assertThat(products).containsExactlyInAnyOrder(...).

Here's why:

  • hasSize(3) would pass for ["Laptop", "Mouse", "Mouse"], but this doesn't verify the content, only the count. It's not specific enough.
  • contains(...) checks that the list contains at least the given elements. It would pass for ["Laptop", "Mouse", "Mouse", "Webcam"], so it doesn't check for exactness.
  • containsExactlyInAnyOrder(...) is the most precise. It checks three things: the size is exactly 3, the elements are exactly the ones specified, and it doesn't care about their order. It would correctly fail for ["Laptop", "Mouse", "Mouse"] because "Keyboard" is missing and "Mouse" is duplicated.

Conclusion

You've now seen how AssertJ can dramatically improve the quality and readability of your tests. By embracing its fluent API, you write assertions that are self-documenting and provide rich feedback when they fail.

Key Takeaways:

  • AssertJ's fluent API (assertThat(...)) makes tests more readable and less error-prone than traditional JUnit assertions.
  • It provides incredibly descriptive error messages that pinpoint the exact cause of a failure.
  • It has a rich set of specialized assertions for common Java types like Strings, Collections, and Maps.
  • Features like extracting(), as(), and soft assertions provide powerful ways to write comprehensive and maintainable tests.

What's Next?

Now that you have a powerful tool for writing assertions, it's time to apply it. In our next lesson, we will dive into writing unit tests for the service layer using JUnit 5 and Mockito. You'll learn how to isolate the business logic in your service classes and use "mocks" to simulate dependencies like repositories, which is a cornerstone of effective unit testing.