Google Truth vs. AssertJ: Wann lohnt sich der Wechsel?

Julian | May 29, 2026 min read

Im ersten Teil dieser Serie haben wir Google Truth von Grund auf angeschaut – warum es existiert, wie das Subject-Modell funktioniert, und wann Assertions lesbar werden statt kryptisch.

Aber da ist noch eine Frage, die ich bewusst ausgeklammert hatte: Was, wenn du schon AssertJ nutzt?

Und das tust du wahrscheinlich. Jedes Spring-Boot-Projekt zieht spring-boot-starter-test rein, und darin steckt AssertJ. Es ist einfach da. Fluent API, gute IDE-Unterstützung, jahrelang bewährt. Kein schlechtes Ausgangsmaterial.

Dieser Artikel ist kein Plädoyer für einen Wechsel. Es geht um die ehrliche Antwort auf die Frage: Wo unterscheiden sich die beiden Tools wirklich – und wann macht es Sinn, Truth bewusst zu wählen, obwohl AssertJ schon vorhanden ist?

AssertJ: Das, was du vermutlich schon nutzt

AssertJ ist seit Jahren der De-facto-Standard für Assertions in der Java-Welt – nicht weil es sich durchgesetzt hat, sondern weil Spring Boot es mitbringt. Wenn du spring-boot-starter-test in deinem pom.xml hast, ist AssertJ bereits da:

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

Kein weiterer Eintrag nötig. Kein bewusster Entscheid. AssertJ ist einfach Teil des Spring-Ökosystems.

Das ist auch der Grund, warum die meisten Teams es benutzen – und das ist vollkommen legitim.

Was AssertJ gut kann

Eine breite API-Oberfläche. AssertJ bringt built-in Assertions für fast alles mit: Strings, Collections, Maps, Optionals, Dates, Files, Exceptions, Iterables. Du wirst selten in eine Situation kommen, wo dir ein Assertion-Typ fehlt.

// 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-Unterstützung, die tatsächlich hilft. AssertJ ist darauf ausgelegt, dass du über Autocomplete entdeckst, was möglich ist. Schreib assertThat(myList). und deine IDE zeigt dir sofort alle sinnvollen Methoden für diesen Typ. Das senkt die Einstiegshürde erheblich.

Fehlermeldungen, die passen. Nicht perfekt, aber deutlich besser als rohe JUnit-Ausgaben:

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

Bei Collections wird es informativer:

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

Was AssertJ nicht macht

AssertJ ist mächtig – aber es löst keine Probleme, die du noch nicht hattest. Wenn deine Fehlermeldungen klar sind, wenn dein Team mit der API vertraut ist und wenn du keine komplexe Domäne hast, gibt es keinen zwingenden Grund, etwas zu ändern.

Der Unterschied zu Truth zeigt sich erst in einem bestimmten Bereich: wie du eigene Assertion-Typen für deine Domain-Objekte baust. Dazu kommen wir weiter unten.

Google Truth: Wo es einen anderen Weg geht

Google Truth löst dasselbe Problem wie AssertJ – aber mit einer anderen Philosophie dahinter. Und die macht sich erst bemerkbar, wenn du über einfache Assertions hinausgehst.

Weniger API, mehr Struktur

Truth hat eine bewusst schlankere Core-API als AssertJ. Nicht weil Features fehlen, sondern weil Truth anders kategorisiert: Jeder Typ bekommt seinen eigenen Subject, und der Subject definiert genau die Assertions, die für diesen Typ sinnvoll sind.

Der Einstieg ist immer 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-technisch kaum ein Unterschied zu AssertJ. Der eigentliche Unterschied liegt woanders.

Fehlermeldungen: Truth geht einen Schritt weiter

Truth investiert mehr in die Struktur der Fehlermeldung. Statt nur „erwartet vs. tatsächlich" bekommst du bei Collections eine aufgeschlüsselte Analyse:

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

Truth sagt dir nicht nur was falsch ist – es sagt dir was fehlt und was nicht erwartet wurde. Gerade in längeren Listen ist das der Unterschied zwischen sofort und nach einer Minute Suchen.

assertWithMessage(): Kontext direkt in die Fehlermeldung

Ein kleines, aber nützliches Feature: Du kannst jeder Assertion eine Beschreibung voranstellen.

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

AssertJ hat as() dafür – die Funktionalität ist vergleichbar. Truth macht es syntaktisch etwas expliziter durch das vorangestellte assertWithMessage.

Das Kernfeature: Custom Subjects

Das ist der Punkt, der Truth von anderen Assertion-Libraries unterscheidet. Du kannst eigene Subject-Klassen schreiben – typsichere Assertion-Wrapper für deine eigenen Domain-Objekte.

Das klingt nach mehr Aufwand. Ist es auch. Aber der Unterschied zur AssertJ-Variante ist fundamental genug, dass er einen eigenen Abschnitt bekommt.

Zuerst der direkte Vergleich – dann die Custom-Implementierungen.

Direktvergleich: gleiche Assertion, beide Tools

Theorie beiseite. Hier sehen wir beide Libraries an denselben Szenarien – damit du selbst urteilen kannst, wo sich was anfühlt.

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);

Fazit: Nahezu identisch. AssertJ erlaubt längere Chains auf einem Objekt, Truth bricht das in separate Assertions auf. Geschmackssache.

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");

Fazit: AssertJ punktet beim Chaining – ein Ausdruck, eine Collection, viele Checks. Truth ist expliziter, aber etwas gesprächiger.

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");

Fazit: Identische Methoden-Namen, unterschiedliche Chaining-Strategie.

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");

Fazit: Das ist ein echter Unterschied. AssertJ hat assertThatThrownBy() built-in und das ist elegant. Truth hat keinen direkten Ersatz – du kombinierst JUnit 5 assertThrows mit Truth-Assertions auf dem Exception-Objekt. Funktioniert, aber AssertJ ist hier komfortabler.

Fehlermeldungen im Vergleich

Derselbe fehlerhafte Test, beide 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 strukturiert die Fehlermeldung einen Tick klarer – besonders das explizite missing / unexpected hilft bei größeren Listen. AssertJ ist trotzdem gut lesbar.

Zwischenfazit

Für Standardfälle – Strings, Collections, Maps, Primitives – sind beide Libraries nahezu gleichwertig. AssertJ gewinnt beim Chaining und bei Exception-Assertions. Truth hat die etwas strukturierteren Fehlermeldungen.

Der eigentliche Unterschied liegt nicht in diesen Built-in-Assertions. Er liegt darin, was passiert, wenn du deine eigenen Domain-Objekte assertieren willst.

Custom Subjects vs. Custom Conditions – der eigentliche Unterschied

Das ist der Abschnitt, der die Entscheidung meistens klärt.

Beide Libraries erlauben dir, eigene Assertions für deine Domain-Objekte zu schreiben. Aber sie gehen das fundamental anders an. Schauen wir uns beide Varianten an derselben Order-Klasse an:

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

    // Getter...
}

Der AssertJ-Weg: AbstractAssert erweitern

AssertJ nutzt Vererbung. Du erstellst eine Klasse, die AbstractAssert erweitert, und bekommst damit Zugriff auf alle Built-in-Methoden von AssertJ – plus deine eigenen.

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;
    }
}

Im Test:

import static com.example.OrderAssert.assertThat;

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

Das Gute daran: Du kannst alles chainen. isPending().hasTotal(...).containsItem(...) – ein Ausdruck, ein Objekt. AssertJ-Nutzer kennen das Muster sofort.

Das weniger Gute: failWithMessage() ist manuell. Du schreibst die Fehlermeldung selbst als String. Wenn du dabei einen Typo machst oder den falschen Wert einbettest, merkst du es erst wenn der Test fehlschlägt. Truth löst das anders.

Der Truth-Weg: Subject + Factory

Truth nutzt keine Vererbungshierarchie mit Built-in-Methoden. Stattdessen bekommst du check() – einen Mechanismus, der Truth-eigene Assertions auf einzelne Properties delegiert und dabei automatisch den Pfad zur fehlgeschlagenen Property in die Fehlermeldung einbettet.

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);
    }
}

Im Test:

import static com.example.OrderSubject.assertThat;

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

Was check() dir gibt: Wenn isPending() fehlschlägt, sieht die Fehlermeldung so aus:

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

Truth bettet automatisch den Ausdruck getStatus() in die Fehlermeldung ein – weil du check("getStatus()") geschrieben hast. Keine manuelle Fehlermeldung, kein failWithMessage(). Der Kontext kommt aus der Struktur, nicht aus einem String.

Was das im Alltag bedeutet

AssertJTruth
Chaining✅ Nativ, ein Ausdruck❌ Separate Assertions
FehlermeldungenManuell via failWithMessage()Automatisch via check()
Built-in Methoden✅ Alle AssertJ-Methoden vererbbar❌ Nur Subject-Basis
BoilerplateModeratEtwas mehr (Factory-Pattern)
Typsicherheit

Chaining oder strukturierte Fehlermeldungen – das ist der Trade-off. Wer viele Assertions in einem Ausdruck bündeln will, ist mit AssertJ besser bedient. Wer will, dass Fehlermeldungen präzise auf die Property zeigen ohne manuellen Aufwand, greift zu Truth.

Entscheidungshilfe: Wann welches Tool

Kein falsches „einer gewinnt". Beide Libraries sind gut – sie lösen dasselbe Problem auf unterschiedlichen Wegen. Die Frage ist, welcher Weg zu deinem Projekt passt.

Nimm AssertJ wenn…

…du ein Spring-Boot-Projekt hast. AssertJ ist bereits da. Kein zusätzlicher Dependency-Eintrag, kein Onboarding, kein „warum haben wir zwei Assertion-Libraries?"-Meeting. Das ist kein schlechter Grund.

…dein Team stark auf IDE-Autocomplete setzt. AssertJ ist für Discovery per Autocomplete gebaut. assertThat(myObject). und du siehst sofort, was möglich ist. Das senkt die Einstiegshürde für neue Teammitglieder.

…du hauptsächlich built-in Typen assertierst. Strings, Collections, Exceptions, Dates – AssertJ hat dafür alles. Und assertThatThrownBy() ist bei Exception-Testing komfortabler als Truths JUnit-5-Kombination.

…Chaining für dich lesbar ist. Wenn du gerne schreibst:

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

…dann ist AssertJ genau dafür gemacht.

Nimm Truth wenn…

…du eine team-weite Assertion-DSL für deine Domain aufbaust. Truths check()-Mechanismus macht Custom Subjects zu wartbarem Produktionscode. Fehlermeldungen sind automatisch präzise, ohne dass jeder Entwickler beim Schreiben von failWithMessage() aufpassen muss.

…du viele Domain-Objekte hast, die in vielen Tests auftauchen. Je öfter du dasselbe Objekt assertierst, desto mehr lohnt sich ein Custom Subject. Ein OrderSubject, ein UserSubject, ein InvoiceSubject – und deine Tests lesen sich wie Spezifikationen.

…du in einem Google-Stack oder bibliotheksnahen Projekt arbeitest. Truth ist Googles internes Standard-Tool. Wer Guava, Dagger oder andere Google-Libraries nutzt, bewegt sich ohnehin in diesem Ökosystem.

…du Truth bereits aus dem Einführungsartikel kennst und die Fehlermeldungen überzeugend findest. Manchmal reicht das als Einstieg.

Die pragmatische Antwort

Für die meisten Spring-Boot-Projekte: Bleib bei AssertJ. Es ist vorhanden, gut dokumentiert, IDE-freundlich, und die Fehlermeldungen sind ausreichend klar.

Der Moment, wo Truth interessant wird, ist spezifisch: Du merkst, dass du immer wieder dieselben Domain-Objekte assertierst, deine Custom-AssertJ-Klassen wachsen, und die Fehlermeldungen bei Property-Checks ungenau werden. Dann ist check() die Antwort – und Truth lohnt sich als zweite Dependency.

Beide Libraries parallel zu nutzen ist übrigens kein Tabu. Truth für Domain-Subjects, AssertJ für alles andere – das ist eine legitime Strategie.

Fazit

AssertJ und Google Truth lösen dasselbe Problem – und beide lösen es gut. Der Unterschied liegt nicht in der Syntax, die ist zu ähnlich um eine Entscheidung zu rechtfertigen. Er liegt darin, wie du mit deinen eigenen Domain-Objekten umgehst und was du von einer Fehlermeldung erwartest.

AssertJ ist die vernünftige Standardwahl im Spring-Ökosystem. Es ist einfach da, es ist mächtig, und es kostet kein Onboarding. Truth ist kein Upgrade – es ist eine Alternative mit einem spezifischen Vorteil: Custom Subjects, die Fehlermeldungen ohne manuellen Aufwand präzise machen.

Wenn deine Tests heute klar sind und deine Fehlermeldungen dir sagen, was du wissen musst – ändere nichts. Wenn du merkst, dass du Domain-Objekte in Dutzenden von Tests assertierst und die Fehlermeldungen zu unspezifisch werden: Das ist der Moment, Truth auszuprobieren.

Im nächsten Teil der Serie schauen wir uns an, wie du Custom Subjects produktionsreif baust – mit Factory-Pattern, Fehlerbehandlung und Tests für deine Assertions selbst.