1. Der Schmerz mit einfachen JUnit-6-Assertions
Wenn du gerade anfängst, Tests zu schreiben, scheinen assertEquals, assertTrue und assertFalse völlig ausreichend. Aber sobald du nicht mehr „1 + 1 = 2" prüfst, sondern das Verhalten von echtem Businesscode – und sei er noch so klein –, wächst die Anzahl der Prüfungen schnell. Und plötzlich sieht der Test nicht mehr wie ein Szenario aus, sondern wie ein Buchhaltersbericht.
Stellen wir uns ein sehr einfaches Ergebnis vor: Wir haben den Slug eines Artikels (in ContentHub wird der Slug für die URL benötigt und soll lesbar sein). Scheinbar ein gewöhnlicher String. Aber selbst hier möchte man meist mehr als eine Eigenschaft prüfen: dass der Slug dem erwarteten Wert entspricht, mit dem richtigen Präfix beginnt, keine Leerzeichen enthält und nicht leer ist. Mit blanken JUnit-Assertions wird das schnell zu einem Haufen einzelner Aufrufe, in dem das Auge keinen Sinn mehr erkennt.
Hier ist ein Beispiel im Stil „reines JUnit 6". Es ist nicht schlecht – es ist nur… laut:
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class SlugFormatTest {
@Test
void slug_is_readable_for_url() {
// Tatsächlicher Wert, den wir prüfen (actual)
String slug = "java-basics";
// Prüfen des erwarteten Werts als Ganzes
Assertions.assertEquals("java-basics", slug);
// Prüfung einzelner Eigenschaften, die aber wie unabhängige Aussagen wirken
Assertions.assertTrue(slug.startsWith("java"));
Assertions.assertFalse(slug.contains(" "));
}
}
Das Problem liegt nicht darin, dass der Code falsch ist. Das Problem liegt darin, dass er sich wie „drei unabhängige Aussagen" liest und nicht wie „ein erwartetes Verhalten". Und wenn ein solcher Test fehlschlägt, ist die Fehlermeldung oft zu allgemein: Du siehst, dass assertTrue fehlgeschlagen ist, aber es ist nicht immer sofort klar, was genau geprüft wurde und warum es wichtig ist.
Genau hier kommt AssertJ ins Spiel: Es erlaubt, Prüfungen als zusammenhängende Aussage über das Ergebnis zu schreiben und nicht als eine Menge einzelner „Pings" in Richtung Assertions.*.
2. AssertJ als Grundstil des Kurses
AssertJ ist eine Assertions-Bibliothek mit Fluent-API, also mit „Ketten" von Aufrufen. Dadurch lesen sich Prüfungen fast wie Sätze in einer menschlichen Sprache. „Fast" – weil es trotzdem Java ist und nicht Deutsch. Aber der Fortschritt ist spürbar: Ein Test wird näher an einer Verhaltensbeschreibung als an einem technischen Prüfprotokoll.
Ein wichtiger praktischer Punkt: In einem typischen Spring Boot-Projekt (und auch in unserem Lehrprojekt ContentHub) ist AssertJ in den Testabhängigkeiten meist bereits vorhanden, weil spring-boot-starter-test es mitbringt. Du musst also nicht „den Teststack manuell zusammenstellen", als würdest du in einem RPG ein Schwert schmieden. Es reicht, zu verstehen, wie man es benutzt.
Warum AssertJ der „Standardstil" des Kurses wird:
Erstens hilft die Fluent-Kette, den Fokus auf den Sinn zu halten: Du beginnst mit dem tatsächlichen Wert und hängst Erwartungen daran. Zweitens sind die Fehlermeldungen von AssertJ in der Regel informativer: Die Bibliothek versucht, den tatsächlichen Wert, den erwarteten und den Kontext anzuzeigen. Drittens skaliert dieser Stil gut: Dieselben Prinzipien funktionieren für Strings, Kollektionen, verschachtelte Objekte und JSON. Man muss die „Schreibweise" nicht jedes Mal wechseln.
Direkt gesagt: JUnit-Assertions verschwinden nicht. Aber in diesem Kurs gilt eine einfache Regel: Wenn du unsicher bist, womit du prüfen sollst – fange mit AssertJ an.
3. Einstiegspunkt: assertThat(actual)
Wenn Leute AssertJ zum ersten Mal sehen, denken sie oft: „Ok, neue Bibliothek – also neue magische Methoden". Die Kernidee ist aber ganz einfach: Du beginnst immer mit assertThat(...), dem du den tatsächlichen Wert übergibst, und beschreibst dann, wie er sein soll.
Hier gilt es, eine „eiserne" Regel zu merken, die vor dummen Fehlern schützt: In assertThat(...) steckst du den actual – also das, was der Code produziert hat. Nicht expected. Nicht „wie es sein soll". Sondern das, was nach dem Act-Teil des Tests tatsächlich entstanden ist.
Ein Beispiel mit minimaler Konfiguration (ich zeige die Imports sofort, damit assertThat nicht wie Gedankenübertragung wirkt):
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class AssertJIntroTest {
@Test
void assert_that_starts_from_actual_value() {
// Arrange: wir haben ein Beispielergebnis vorbereitet
String slug = "java-basics";
// Assert: wir beginnen mit actual (slug) und beschreiben die Erwartung
assertThat(slug).isEqualTo("java-basics");
}
}
Das liest sich so: „Ich behaupte, dass slug gleich "java-basics" ist." Und darauf lässt sich eine Kette aufbauen.
Wenn du jemals festgestellt hast, dass du assertEquals(expected, actual) schreibst und die Reihenfolge der Argumente ständig verwechselst, sagt AssertJ quasi: „Lass mich hier die Chance auf einen Fehler entfernen". In AssertJ ist die Reihenfolge natürlicher: erst der tatsächliche Wert, dann die Erwartungen.
4. Fluent-Ketten für zusammenhängende Prüfungen
Das Schönste an AssertJ ist die Möglichkeit, mehrere Prüfungen zu einer Kette zusammenzufügen, wenn sie zu demselben semantischen Objekt gehören. Zum Beispiel muss ein Slug dem erwarteten Wert entsprechen, mit dem richtigen Wort beginnen und keine Leerzeichen enthalten. Das sind drei Prüfungen, aber alle betreffen dieselbe Qualität: „Der Slug ist URL-tauglich und entspricht der Regel".
Hier ist dasselbe Slug-Beispiel, jetzt im AssertJ-Stil:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class SlugFormatAssertJTest {
@Test
void slug_is_readable_for_url() {
// Actual: das Ergebnis, das wir prüfen
String slug = "java-basics";
// Eine Kette = ein Sinn: "Slug ist URL-tauglich"
assertThat(slug)
.isEqualTo("java-basics") // exakter Wert
.startsWith("java") // benötigtes Präfix
.doesNotContain(" "); // keine Leerzeichen
}
}
Der Test liest sich wie eine kleine Geschichte: „Slug ist gleich…, beginnt mit…, enthält nicht…". Sofort sichtbar, dass alle Prüfungen dasselbe betreffen.
Nun ein Beispiel mit Zahlen. In ContentHub können Anhänge Einschränkungen haben: zum Beispiel „nicht mehr als 3 Anhänge pro Artikel" (die genaue Zahl ist jetzt nicht wichtig; wichtig ist, dass es ein Limit gibt). Zahlenprüfungen mit JUnit werden oft zu assertTrue(count <= 3), und das klingt wie „wir haben irgendwo etwas mit etwas verglichen". AssertJ bietet eine lesbarere Form.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class AttachmentLimitsTest {
@Test
void attachment_count_is_within_limit() {
// Beispiel der tatsächlichen Anzahl von Anhängen
int attachmentCount = 2;
// Wir prüfen den Sinn (Einschränkung), nicht die "Formel"
assertThat(attachmentCount)
.isPositive() // keine negative Zahl
.isLessThanOrEqualTo(3); // überschreitet das Limit nicht
}
}
Hier sieht man bereits, dass wir nicht die „Formel" prüfen, sondern den Sinn: Die Zahl ist positiv und überschreitet das Limit nicht.
Noch ein kleines Beispiel – Boolean. Manchmal gibt es im Businesscode ein Flag, und du möchtest den Test buchstäblich als „muss wahr sein" lesen. AssertJ erlaubt das ohne das Gefühl, Mathematik zu lesen.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class BooleanAssertionsTest {
@Test
void article_can_be_published_flag_is_true() {
// Actual: Flag aus der Geschäftslogik (hier: einfach eine Konstante)
boolean canBePublished = true;
// Assert: liest sich fast wie ein Satz
assertThat(canBePublished).isTrue();
}
}
Ja, das wirkt trivial. Aber wenn es viele solcher Prüfungen gibt, beginnt der „menschliche" Stil, Aufmerksamkeit zu sparen. Und Aufmerksamkeit ist die knappste Ressource eines Entwicklers (nach Kaffee).
Wichtig: nicht übertreiben. In eine Fluent-Kette sollte man nur semantisch zusammenhängende Prüfungen packen. Wenn du mit dem Slug beginnst, dann plötzlich den Artikelstatus prüfst und danach die Anzahl der Anhänge – das sind bereits drei verschiedene Bedeutungen, und die Kette wird zum Durcheinander. AssertJ bietet Komfort, hebt aber den gesunden Menschenverstand nicht auf.
5. .as(...) und verständliche Fehlschläge
Wenn ein Test fehlschlägt, siehst du normalerweise zwei Dinge: den Stacktrace und die Fehlermeldung. Der Stacktrace in Tests erfreut selten das Herz, daher hängt die „Lebensqualität" stark davon ab, wie verständlich die Meldung ist, die du bekommst.
AssertJ erlaubt es, eine Beschreibung der Prüfung über .as(...) hinzuzufügen. Das ändert nichts an der Logik, macht einen Fehlschlag aber konkreter: Statt „expected X, but was Y" siehst du, was genau geprüft wurde und in welchem Kontext.
Stellen wir uns vor, wir prüfen den Status eines neuen Artikels. In ContentHub wird ein neuer Artikel gemäß dem Szenario als DRAFT erstellt. Auch wenn der Test selbst noch sehr vereinfacht ist, bringt die Beschreibung ihn näher an die Fachdomäne.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ArticleStatusTest {
@Test
void new_article_starts_as_draft() {
// Actual: Status, den der Code zurückgegeben hat (hier: einfach ein String)
String status = "DRAFT";
// Die Beschreibung hilft, den Fehlschlag zu verstehen: was genau wurde geprüft
assertThat(status)
.as("Status des neuen Artikels")
.isEqualTo("DRAFT");
}
}
Der Sinn von .as(...) zeigt sich besonders, wenn der Test länger wird und mehrere Prüfungen enthält. Wenn eine davon fehlschlägt, siehst du in der Meldung sofort „Status des neuen Artikels" und musst nicht rätseln, was das für eine Variable status war und warum sie hier überhaupt aufgetaucht ist.
Ein Hinweis: Die Beschreibung sollte fachbezogen sein. Wenn du "test" oder "check" schreibst, hilft das nicht. Eine gute Beschreibung beantwortet die Frage „Was ist das für ein Wert in der Domäne": Artikel-Status, Slug für die URL, Anhang-Limit, Kategoriename usw.
6. AssertJ anhand von ContentHub-Beispielen
Prüfsprache trainiert man am besten an „lebendigen" Domänenbeispielen und nicht an namenlosen foo/bar. Daher nehmen wir drei sehr typische Dinge aus ContentHub: Slug, Artikel-Status und die Prüfung einer einfachen Einschränkung.
Beispiel: minimaler SlugService und Test
Damit das Beispiel in sich selbst vollständig ist, bauen wir einen winzigen Service. Er ist nicht „ideal", er ist lehrreich: Er wandelt einen String in Kleinbuchstaben um, schneidet Ränder ab und ersetzt Leerzeichen durch Bindestriche. Das Wichtigste ist, dass wir ein Objekt haben, das ein Ergebnis zurückgibt, das sinnvoll geprüft werden kann.
class SlugService {
String toSlug(String title) {
// Lehrhafte Implementierung: Schreibweise normalisieren und "URL-ähnlichen" String erzeugen
return title.toLowerCase()
.trim()
.replace(" ", "-");
}
}
Und nun der Test. Beachte: Wir beschränken uns nicht auf isEqualTo, sondern prüfen die „URL-Tauglichkeitseigenschaften". Darin liegt die Stärke der Fluent-Ketten: Ein Sinn – mehrere Bedingungen.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class SlugServiceTest {
@Test
void toSlug_makes_lowercase_and_uses_hyphens() {
// Arrange: Service erstellen
SlugService slugService = new SlugService();
// Act: tatsächliches Ergebnis holen (actual)
String slug = slugService.toSlug("Java Basics");
// Assert: mehrere zusammenhängende Anforderungen an den Slug prüfen
assertThat(slug)
.as("Artikel-Slug für die URL")
.isEqualTo("java-basics") // erwartetes Format
.doesNotContain(" ") // keine Leerzeichen
.doesNotStartWith("-"); // kein seltsames Präfix
}
}
Wir machen hier etwas mehr als einen einfachen Gleichheitsvergleich. Das ist nützlich: Wenn die Service-Implementierung sich ändert (z.B. jemand vergisst trim()), fängt der Test das Problem dem Sinn nach auf und nicht nur durch „Strings stimmen nicht überein".
Beispiel: Artikel-Status als Teil einer Regel
Nehmen wir an, wir haben eine Enum der Statuswerte (im Projekt wird es sie sicher geben, aber jetzt machen wir eine kleine Vorlage):
enum ArticleStatus {
// Mögliche Zustände eines Artikels in der Domäne
DRAFT,
IN_REVIEW,
PUBLISHED,
REJECTED,
ARCHIVED
}
Und nehmen wir an, ein neuer Artikel wird standardmäßig als DRAFT erstellt. Wieder: Das Beispiel ist lehrreich, aber nah an der Realität.
class Article {
// Status als Teil des Entitätszustands speichern
private final ArticleStatus status;
Article(ArticleStatus status) {
// In einem echten Projekt gibt es hier meist noch Invarianten und Validierung
this.status = status;
}
ArticleStatus getStatus() {
// Getter – das, was der Test benutzt (actual holen wir von hier)
return status;
}
}
Der Test mit AssertJ bleibt einfach und lesbar:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ArticleTest {
@Test
void new_article_is_draft() {
// Arrange/Act: Artikel mit bekanntem Status erstellen
Article article = new Article(ArticleStatus.DRAFT);
// Assert: mit dem erwarteten Domänenwert vergleichen
assertThat(article.getStatus())
.as("Artikel-Status bei der Erstellung")
.isEqualTo(ArticleStatus.DRAFT);
}
}
Warum ist das ein gutes Beispiel für die AssertJ-Vorlesung? Weil es das Kernprinzip zeigt: assertThat bekommt actual – das, was der Code zurückgegeben hat –, und dann formulieren wir die Erwartung in Domänenbegriffen, nicht im Stil „wir haben irgendwo zwei Dinge verglichen".
Beispiel: einfache Einschränkung als lesbare Prüfung
Nehmen wir die Dateigrößen-Einschränkung, aber ohne Dateisystem und ohne I/O. Nehmen wir an, die Methode akzeptiert eine Größe und sagt, ob sie zulässig ist. Vereinfacht: bis 5 MB ist erlaubt.
class AttachmentValidationService {
boolean isSizeAllowed(long sizeBytes) {
// Obere Grenze der Größe (5 MB)
long max = 5 * 1024 * 1024;
// Regel: Größe darf das Limit nicht überschreiten
return sizeBytes <= max;
}
}
Der Test wird auch hier lesbarer, wenn wir nicht die „Formel" prüfen, sondern die Tatsache:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class AttachmentValidationServiceTest {
@Test
void size_is_allowed_when_under_limit() {
// Arrange
AttachmentValidationService service = new AttachmentValidationService();
// Act: kleine Größe prüfen
boolean allowed = service.isSizeAllowed(1024);
// Assert: wir wollen "ja" erhalten
assertThat(allowed)
.as("Eine kleine Datei sollte erlaubt sein")
.isTrue();
}
}
Ja, jemand wird sagen: „Hier hätte man auch assertTrue(allowed) verwenden können". Man kann. Aber erstens üben wir einen einheitlichen Stil. Zweitens macht .as(...) einen Fehlschlag sinnvoll. Und drittens gibt dir die Gewohnheit, überall assertThat zu schreiben, einen gleichmäßigen, homogenen Testsatz.
7. AssertJ in Arrange–Act–Assert
Manchmal scheint es, dass wenn wir Assertions schön machen, der Test automatisch gut wird. Fast stimmt das, aber es gibt eine Nuance: Ein guter Test besteht nicht nur aus „was wir prüfen", sondern auch aus „wie wir dazu gekommen sind". Deshalb halten wir an AAA fest, und AssertJ verwenden wir als beste Option für den dritten Teil – Assert.
Sieh dir den Test unten an: Er ist durch Kommentare in drei semantische Blöcke aufgeteilt. Das ist nicht obligatorisch, hilft Anfängern aber, den Überblick zu behalten. Und die AssertJ-Kette am Ende macht die Prüfung kompakt und geordnet.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class SlugServiceAaaTest {
@Test
void toSlug_generates_url_friendly_value() {
// Arrange
SlugService slugService = new SlugService();
// Act
String slug = slugService.toSlug(" Spring Boot ");
// Assert
assertThat(slug)
.as("Slug sollte URL-tauglich sein")
.isEqualTo("spring-boot")
.doesNotContain(" ");
}
}
Methodisch wichtig: Der Test liest sich von oben nach unten wie ein Szenario. Du springst nicht mit den Augen zwischen assertTrue, assertFalse und assertEquals hin und her und versuchst, das Gesamtbild zusammenzusetzen. Du siehst sofort, dass das Ergebnis slug mehreren zusammenhängenden Eigenschaften genügen muss, und sie liegen alle nebeneinander – in einer Kette.
Das ist übrigens noch ein versteckter Vorteil von Fluent-Ketten: Sie helfen zu bemerken, dass man zu viel prüft. Wenn die Kette auf 12 Bedingungen anwächst, prüft man entweder mehrere verschiedene Bedeutungen auf einmal (Zeit, in mehrere Assertions aufzuteilen) oder das Szenario selbst ist zu breit geworden (Zeit, es zu vereinfachen).
8. Spickzettel für Prüfungen
Wenn man anfängt, AssertJ zu benutzen, verlangt das Gehirn zunächst nach einer „Liste aller Methoden". Das ist eine normale Reaktion, aber nutzlos: Die Bibliothek ist groß und du wirst es ohnehin nicht alles auf einmal lernen. Viel nützlicher ist ein kleiner Satz „täglicher" Prüfungen, der 80% der Fälle zu Beginn des Kurses abdeckt.
| Ergebnistyp | Beispiele für AssertJ-Prüfungen | Mini-Beispiel |
|---|---|---|
| Strings | isEqualTo, contains, startsWith, endsWith, doesNotContain, isBlank, isNotBlank | assertThat(slug).startsWith("java").doesNotContain(" "); |
| Zahlen | isZero, isPositive, isNegative, isLessThan, isLessThanOrEqualTo, isGreaterThan, isBetween | assertThat(count).isPositive().isLessThanOrEqualTo(3); |
| Boolean | isTrue, isFalse | assertThat(allowed).isTrue(); |
| null / nicht null | isNull, isNotNull | assertThat(article).isNotNull(); |
Diese Tabelle ist nicht die „letzte Wahrheit", sondern ein Starterset, mit dem man bereits verständliche Tests schreiben kann. Alles Weitere kannst du schrittweise hinzufügen, wenn die Prüfungen komplexer werden, damit das Lernen nicht zur Enzyklopädie wird.
9. Typische Fehler beim Wechsel zu AssertJ
Fehler Nr. 1: In assertThat(...) wird expected statt actual gesteckt.
Das ist die klassische Verwechslung nach assertEquals(expected, actual). In AssertJ beginnt alles vom Tatsächlichen: Du prüfst das, was entstanden ist. Wenn du expected in assertThat steckst, kann der Test bedeutungslos werden – oder schlimmer, immer grün sein, weil du die Erwartung quasi gegen sich selbst prüfst. Das lässt sich einfach beheben: Sag dir laut „assertThat – das bedeutet 'Ich behaupte, dass DIESER Wert…'".
Fehler Nr. 2: In einer Kette werden semantisch unzusammenhängende Prüfungen verknüpft.
Eine Fluent-Kette bedeutet nicht „schreib alles hintereinander wie in einem Zug". Wenn du Slug, Status und Anhang-Limit in einer Kette prüfst, macht du den Test nicht kompakter – du machst ihn unlesbar. Eine Kette ist gut, wenn alle Bedingungen denselben Qualitätsaspekt eines Ergebnisses beschreiben. Wenn mehrere Bedeutungen vorhanden sind, besser mehrere separate Assertions schreiben, jede mit eigenem .as(...).
Fehler Nr. 3: .as(...) wird als dekoratives Etikett "test" benutzt.
.as("test") oder .as("check something") hilft nicht. Die Beschreibung muss fachbezogen sein: „Artikel-Slug für die URL", „Status eines neuen Artikels", „Anzahl-Limit für Anhänge". Wenn der Test dann fehlschlägt, siehst du in der Meldung genau den Sinn, den du schützen wolltest. Das ist besonders wichtig bei einem großen Testsatz, wo es viele ähnliche Prüfungen gibt.
Fehler Nr. 4: AssertJ wird als „einfacher Ersatz für assertEquals" verwendet.
Wenn du assertThat(x).isEqualTo(y) schreibst und das war's – du hast schon einen Schritt in Richtung Lesbarkeit gemacht, aber die Stärke der Bibliothek nicht genutzt. AssertJ ist gut darin, Eigenschaften des Ergebnisses zu prüfen. Zum Beispiel ist es für einen Slug nützlich, nicht nur die Gleichheit, sondern auch das Fehlen von Leerzeichen, das korrekte Format und das Fehlen seltsamer Präfixe zu prüfen. Das heißt, du fixierst das Verhalten, nicht nur einen konkreten Wert.
Fehler Nr. 5: Der Test wird „zu klug" und beginnt, Überflüssiges zu prüfen.
AssertJ hat sehr viele Methoden, und es ist verlockend, alles zu prüfen: startsWith, endsWith, matches, die Länge und noch zehn Bedingungen „auf jeden Fall". Das Ergebnis: Der Test wird fragil – das Ausgabeformat ändert sich, und die Hälfte des Testsatzes fällt, obwohl das echte Risiko nicht gestiegen ist. Eine gute Prüfung ist diejenige, die das Szenario vor einem echten Defekt schützt, nicht diejenige, die „so streng wie möglich ist um der Strenge willen".
GO TO FULL VERSION