[EN] Google Truth vs. AssertJ: When is it worth switching?

Julian | May 29, 2026 min read

In the first part of this series, we looked at Google Truth from the ground up - why it exists, how the subject model works, and when assertions become readable instead of cryptic.

But there is another question that I deliberately left out: What if you already use AssertJ?

And you probably do. Every Spring Boot project pulls in spring-boot-starter-test, and in there is AssertJ. It’s just there. Fluent API, good IDE support, proven for years. Not bad source material.

This article is not a plea for change. It’s about the honest answer to the question: Where do the two tools really differ - and when does it make sense to consciously choose Truth even though AssertJ is already available?

AssertJ: The one you probably already use

AssertJ has been the de facto standard for assertions in the Java world for years - not because it has become established, but because Spring Boot brings it with it. If you have spring-boot-starter-test in your pom.xml, AssertJ is already there:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

No further entry necessary. Not a conscious decision. AssertJ is simply part of the Spring ecosystem.

That’s why most teams use it - and it’s completely legitimate.

What AssertJ is good at

A broad API interface. AssertJ comes with built-in assertions for almost everything: Strings, Collections, Maps, Optionals, Dates, Files, Exceptions, Iterables. You will rarely find yourself in a situation where you are missing an assertion type.

// Strings
assertThat("hello world")
    .startsWith("hello")
    .endsWith("world")
    .hasSize(11);

// Collections
assertThat(List.of("apple", "banana", "cherry"))
    .hasSize(3)
    .contains("banana")
    .doesNotContain("mango");

// Exceptions
assertThatThrownBy(() -> service.findById(-1L))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("invalid id");

IDE support that actually helps. AssertJ is designed to help you discover what’s possible via autocomplete. Type assertThat(myList). and your IDE will immediately show you all sensible methods for this type. This significantly lowers the barrier to entry.

Error messages that fit. Not perfect, but significantly better than raw JUnit output:

org.opentest4j.AssertionFailedError:
expected: "banana"
 but was: "cherry"

Things get more informative with Collections:

Expecting ArrayList:
  ["apple", "cherry"]
to contain:
  ["banana"]
but could not find the following element(s):
  ["banana"]

What AssertJ doesn’t do

AssertJ is powerful - but it doesn’t solve problems you haven’t had before. If your error messages are clear, if your team is familiar with the API, and if you don’t have a complex domain, there’s no compelling reason to change anything.

The difference to Truth only becomes apparent in one specific area: how you build your own assertion types for your domain objects. We’ll get to that below.

Google Truth: Where there is a different path

Google Truth solves the same problem as AssertJ – but with a different philosophy behind it. And this only becomes noticeable when you go beyond simple assertions.

Less API, more structure

Truth has a deliberately leaner core API than AssertJ. Not because features are missing, but because Truth categorizes differently: Each type gets its own Subject, and the Subject defines exactly the assertions that make sense for this type.

The start is always assertThat():

import static com.google.common.truth.Truth.assertThat;

// String
assertThat("hello world").contains("world");
assertThat("hello world").startsWith("hello");

// Collection
assertThat(List.of("apple", "banana")).containsExactly("apple", "banana");

// Map
assertThat(Map.of("key", 42)).containsEntry("key", 42);

Syntax-wise there is hardly any difference to AssertJ. The real difference lies elsewhere.

Error messages: Truth goes one step further

Truth invests more in the structure of the error message. Instead of just “expected vs. actual,” Collections gives you a broken down analysis:

assertThat(List.of("apple", "cherry")).containsExactly("apple", "banana");
value of    : list
missing (1) : [banana]
unexpected  : [cherry]
---
expected    : [apple, banana]
but was     : [apple, cherry]

Truth doesn’t just tell you what is wrong - it tells you what’s missing and what wasn’t expected. Especially in longer lists, this is the difference between searching immediately and after a minute.

assertWithMessage(): Context directly into the error message

A small but useful feature: You can prefix each assertion with a description.

assertWithMessage("User sollte nach Aktivierung aktiv sein")
    .that(user.isActive())
    .isTrue();

AssertJ has as() for this - the functionality is comparable. Truth makes it a little more syntactically explicit by prefixing it with assertWithMessage.

Das Kernfeature: Custom Subjects

This is what sets Truth apart from other assertion libraries. You can write your own Subject classes – type-safe assertion wrappers for your own domain objects.

That sounds like more effort. It is too. But the difference to the AssertJ variant is fundamental enough that it gets its own section.

First the direct comparison - then the custom implementations.

Direct comparison: same assertion, both tools

Theory aside. Here we see both libraries in the same scenarios - so you can judge for yourself what feels right.

Strings

// AssertJ
assertThat("hello world")
    .startsWith("hello")
    .containsIgnoringCase("WORLD")
    .hasSize(11);

// Truth
assertThat("hello world").startsWith("hello");
assertThat("hello world").ignoringCase().contains("world");
assertThat("hello world").hasLength(11);

Conclusion: Almost identical. AssertJ allows longer chains on an object, Truth breaks this up into separate assertions. A matter of taste.

Collections

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

// AssertJ
assertThat(fruits)
    .hasSize(3)
    .contains("banana")
    .doesNotContain("mango")
    .containsExactlyInAnyOrder("cherry", "apple", "banana");

// Truth
assertThat(fruits).hasSize(3);
assertThat(fruits).contains("banana");
assertThat(fruits).doesNotContain("mango");
assertThat(fruits).containsExactlyInAnyOrder("cherry", "apple", "banana");

Conclusion: AssertJ scores points when it comes to chaining – one expression, one collection, many checks. Truth is more explicit but a bit more conversational.

Maps

Map<String, Integer> scores = Map.of("alice", 95, "bob", 87);

// AssertJ
assertThat(scores)
    .containsKey("alice")
    .containsEntry("alice", 95)
    .doesNotContainKey("charlie");

// Truth
assertThat(scores).containsKey("alice");
assertThat(scores).containsEntry("alice", 95);
assertThat(scores).doesNotContainKey("charlie");

Conclusion: Identical method names, different chaining strategy.

Exceptions

// AssertJ
assertThatThrownBy(() -> service.findById(-1L))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("invalid id");

// Truth
// Truth hat kein natives assertThatThrownBy –
// hier bleibt assertThrows aus JUnit 5 die bessere Wahl
Exception ex = assertThrows(IllegalArgumentException.class,
    () -> service.findById(-1L));
assertThat(ex).hasMessageThat().contains("invalid id");

Conclusion: That’s a real difference. AssertJ has assertThatThrownBy() built-in and that is elegant. Truth has no direct replacement - you combine JUnit 5 assertThrows with Truth assertions on the Exception object. Works, but AssertJ is more convenient here.

Error messages in comparison

Same broken test, both libraries:

// Tatsächliche Liste: ["apple", "cherry"]
// Erwartet:          ["apple", "banana"]

AssertJ:

Expecting ArrayList:
  ["apple", "cherry"]
to contain:
  ["banana"]
but could not find the following element(s):
  ["banana"]

Truth:

value of    : list
missing (1) : [banana]
unexpected  : [cherry]
---
expected    : [apple, banana]
but was     : [apple, cherry]

Truth structures the error message a bit more clearly - the explicit ‘missing’ / ‘unexpected’ is particularly helpful for larger lists. AssertJ is still easy to read.

Interim conclusion

For standard cases – strings, collections, maps, primitives – both libraries are almost equivalent. AssertJ wins in chaining and exception assertions. Truth has the slightly more structured error messages.

The real difference is not in these built-in assertions. It lies in what happens when you want to assert your own domain objects.

Custom Subjects vs. Custom Conditions – the real difference

This is the section that usually clarifies the decision.

Both libraries allow you to write your own assertions for your domain objects. But they approach it fundamentally differently. Let’s look at both variants on the same Order class:

public class Order {
    private String status;
    private BigDecimal total;
    private List<String> items;

    // Getter...
}

The AssertJ way: Extend AbstractAssert

AssertJ uses inheritance. You create a class that extends AbstractAssert, giving you access to all of AssertJ’s built-in methods - plus your own.

public class OrderAssert extends AbstractAssert<OrderAssert, Order> {

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

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

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

    public OrderAssert hasTotal(BigDecimal expected) {
        isNotNull();
        if (!expected.equals(actual.getTotal())) {
            failWithMessage("Expected order total to be <%s> but was <%s>",
                expected, actual.getTotal());
        }
        return this;
    }

    public OrderAssert containsItem(String item) {
        isNotNull();
        if (!actual.getItems().contains(item)) {
            failWithMessage("Expected order to contain item <%s> but items were <%s>",
                item, actual.getItems());
        }
        return this;
    }
}

In the test:

import static com.example.OrderAssert.assertThat;

assertThat(order)
    .isPending()
    .hasTotal(new BigDecimal("49.99"))
    .containsItem("Laptop");

The good thing about it: You can chain anything. isPending().hasTotal(...).containsItem(...) – an expression, an object. AssertJ users know the pattern immediately.

The not so good: failWithMessage() is manual. You write the error message yourself as a string. If you make a typo or embed the wrong value, you won’t notice until the test fails. Truth solves this differently.

Der Truth-Weg: Subject + Factory

Truth does not use an inheritance hierarchy with built-in methods. Instead, you get check() - a mechanism that delegates Truth’s own assertions to individual properties and automatically embeds the path to the failed property in the error message.

public class OrderSubject extends Subject {

    private final Order actual;

    private OrderSubject(FailureMetadata metadata, Order actual) {
        super(metadata, actual);
        this.actual = actual;
    }

    public static Subject.Factory<OrderSubject, Order> orders() {
        return OrderSubject::new;
    }

    public static OrderSubject assertThat(Order order) {
        return Truth.assertAbout(orders()).that(order);
    }

    public void isPending() {
        check("getStatus()").that(actual.getStatus()).isEqualTo("PENDING");
    }

    public void hasTotal(BigDecimal expected) {
        check("getTotal()").that(actual.getTotal()).isEqualTo(expected);
    }

    public void containsItem(String item) {
        check("getItems()").that(actual.getItems()).contains(item);
    }
}

In the test:

import static com.example.OrderSubject.assertThat;

assertThat(order).isPending();
assertThat(order).hasTotal(new BigDecimal("49.99"));
assertThat(order).containsItem("Laptop");

What check() gives you: If isPending() fails, the error message looks like this:

value of    : order.getStatus()
expected    : PENDING
but was     : SHIPPED

Truth automatically embeds the expression getStatus() in the error message - because you wrote check("getStatus()"). No manual error message, no failWithMessage(). The context comes from the structure, not from a string.

What that means in everyday life

AssertJTruth
Chaining✅ Native, one expression❌ Separate assertions
Error MessagesManually via failWithMessage()Automatically via check()
Built-in methods✅ All AssertJ methods inheritable❌ Subject base only
BoilerplateModerateA little more (factory pattern)
Type Safety

Chaining or structured error messages – that is the trade-off. Anyone who wants to bundle many assertions in one expression is better served with AssertJ. If you want error messages to point precisely to the property without manual effort, use Truth.

Decision support: When and which tool

No false “one wins”. Both libraries are good - they solve the same problem in different ways. The question is which path suits your project.

Take AssertJ if…

…you have a Spring Boot project. AssertJ is already there. No additional dependency entry, no onboarding, no “why do we have two assertion libraries?” meeting. That’s not a bad reason.

…your team relies heavily on IDE autocomplete. AssertJ is built for discovery via autocomplete. assertThat(myObject). and you will immediately see what is possible. This lowers the barrier to entry for new team members.

…you mainly assert built-in types. Strings, collections, exceptions, dates – AssertJ has everything for that. And assertThatThrownBy() is more convenient for exception testing than Truth’s JUnit 5 combination.

…Chaining is readable for you. If you enjoy writing:

assertThat(order.getItems())
    .hasSize(3)
    .contains("Laptop")
    .doesNotContain("Maus");

…then AssertJ is made for exactly that.

Take Truth if…

…you build a team-wide assertion DSL for your domain. Truth’s check() mechanism turns custom subjects into maintainable production code. Error messages are automatically precise without any developer having to pay attention when writing failWithMessage().

…you have many domain objects that appear in many tests. The more often you assert the same object, the more worthwhile a custom subject is. An OrderSubject, a UserSubject, an InvoiceSubject - and your tests read like specifications.

…you work in a Google stack or library-related project. Truth is Google’s internal standard tool. Anyone who uses Guava, Dagger or other Google libraries is in this ecosystem anyway.

…you already know Truth from the introductory article and find the error messages convincing. Sometimes that’s enough to get you started.

The pragmatic answer

For most Spring Boot projects: Stick with AssertJ. It’s there, well documented, IDE friendly, and the error messages are clear enough.

The moment where Truth becomes interesting is specific: You notice that you are asserting the same domain objects over and over again, your Custom AssertJ classes are growing, and the error messages in property checks are becoming inaccurate. Then check() is the answer – and Truth is worth using as a second dependency.

By the way, using both libraries in parallel is not taboo. Truth for domain subjects, AssertJ for everything else - that’s a legitimate strategy.

Conclusion

AssertJ and Google Truth solve the same problem – and both solve it well. The difference is not in the syntax, which is too similar to justify a decision. It lies in how you handle your own domain objects and what you expect from an error message.

AssertJ is the reasonable default choice in the Spring ecosystem. It’s just there, it’s powerful, and there’s no onboarding cost. Truth is not an upgrade - it is an alternative with a specific advantage: Custom Subjects that make error messages precise without manual effort.

If your tests are clear today and your error messages tell you what you need to know - don’t change anything. If you find yourself asserting domain objects in dozens of tests and the error messages are becoming too vague: That’s the time to try Truth.

In the next part of the series, we’ll look at how to build custom subjects ready for production - with factory patterns, error handling and tests for your assertions themselves.