
AssertJ ist eine Bibliothek, die Assertions über ein Fluent Interface anbietet. AssertJ kann über Maven oder Gradle in ein Projekt eingebunden und in Kombination mit JUnit verwendet werden.
Vorteile gegenüber JUnit
AssertJ bietet gegenüber JUnit ein Vielfaches an möglichen Assertions. Das verbessert die Lesbarkeit von Tests, da die getesten Ausdrücke kompakter und expliziter sind.
int expected = 1;
int actual = 2;
assertEquals(actual, expected); // JUnit
assertThat(actual).isEqualTo(expected); // AssertJ
Was stimmt nicht in diesem Beispiel? AssertEquals()
erwartet als ersten Parameter das expected
und als zweiten Parameter das actual
. Das ist ohne entsprechende Hilfe aus einer IDE (wie z.B. IntelliJ IDEA) nicht ersichtlich. AssertJ macht es hingegen unmissverständlich, wo das actual
reingegeben werden muss. Die Verwechslung von actual
und expected
im AssertEquals()
führt zu folgender Meldung:
org.opentest4j.AssertionFailedError: expected: <2> but was: <1>
Die Intention war genau andersrum: expected: <1> but was <2>
. In etwas komplexeren Tests kann das schnell zu Verwirrung führen.
Fluent Interface
AssertJ bietet ein sogenanntes Fluent Interface, wie es z.B. auch das Builder Pattern und die Java Streaming API tun. Dadurch lassen sich beliebig viele Assertions auf die gleiche Referenz aneinanderketten. Somit lassen sich Assertions platzsparender schreiben – und weniger Boilerplate Code bedeutet auch bessere Lesbarkeit.
String expected = "true";
String actual = "false";
assertNotNull(actual); // JUnit
assertEquals(expected, actual); // JUnit
assertThat(actual).isNotNull().isEqualTo(expected); // AssertJ
Im Beispiel tut die Prüfung auf null
allerdings nicht wirklich not, da assertEquals()
auch mit null
zurecht kommt. Was aber ist mit NullPointerExceptions
? Nehmen wir der Einfachheit halber an, wir wollen equals()
auf toString()
eines Objektes machen.
Object expected = "true";
Object actual = "false";
assertNotNull(actual); // JUnit
assertEquals(expected.toString(), actual.toString()); // JUnit
assertThat(actual).isNotNull().extracting(Object::toString).isEqualTo(expected); // AssertJ
Für JUnit benötigen wir zwei Assertions. In AssertJ lassen sich über extracting()
auf beliebige Methoden von actual
mit Rückgabewerten weitere Assertions anwenden. Das kann gerade bei Verschachtelungen sehr hilfreich sein. Nehmen wir an wir wollen c
mit d
vergleichen und können über a.getB().getC()
auf c
zugreifen. Wenn a
und b
per Definition null
sein dürfen, dann benötigen wir mit Standard-JUnit-Boardmitteln vier verschiedene Ausdrücke. Hier der Vergleich mit AssertJ:
assertNotNull(a); // JUnit
assertNotNull(a.getB()); // JUnit
assertNotNull(a.getB().getC()); // JUnit
assertEquals(a.getB().getC(), d); // JUnit
assertThat(a).isNotNull().extracting(A::getB) // AssertJ
.isNotNull().extracting(B::getC)
.isNotNull().isEqualTo(d);
Ich bin übrigens der Meinung, dass ein Test nicht einfach nur fehlschlagen sollte, wenn ein Fehler im Code eingebaut worden ist, sondern dass er mit einer aussagekräftigen Assertion-Verletzung fehlschlagen sollte. Daher halte ich es nicht für eine geeignete Lösung auf den Nullcheck zu verzichten. Wenn der Aufruf a.getB().getC()
eine NullPointerException
erzeugt, dann ist nicht ersichtlich, ob a
oder b
null
ist.
Eine kleine Auswahl an Assertions
Neben den erwartbaren Assertions für Zeichenketten werden auch reguläre Ausdrücke unterstützt.
AssertThat("string")
.isNotNull()
.isNotEmpty() // Leerstring
.isNotBlank() // nur Whitespaces
.startsWith("str")
.endsWith("ing")
.contains("trin")
.isEqualTo("string");
assertThat("123").containsOnlyDigits();
assertThat("einstring").doesNotContainAnyWhitespaces();
assertThat("abc123").matches("[a-c]{3}[1-3]{3}"); // Regulärer Ausdruck
Listeneinträge lassen sich per extracting()
ähnlich Stream.map()
weiter übersetzen. Damit lassen sich auch gut fachliche Annahmen prüfen, z.B. dass in einer Bestellung für jede Bestellposition eine positive Menge eingetragen worden ist.
assertThat(List.of("a", "b"))
.isNotNull()
.isNotEmpty()
.isNotInstanceOf(ArrayList.class)
.containsExactly("a", "b")
.doesNotContain("c")
.hasSizeGreaterThan(1)
.hasSizeLessThan(3)
.hasSize(2)
.isSubsetOf(List.of("a", "b", "c"))
.extracting(String::toUpperCase)
.contains("A", "B")
.allSatisfy(s -> s.equals(s.toUpperCase()));
Natürlich gibt’s auch für Datums eigene Assertions.
assertThat(new Date())
.isBeforeOrEqualTo(new Date())
.isInThePast()
.isToday();
Assertions zu Exceptions
Mit JUnit5 sind zum Glück die Zeiten vorbei, in denen man seine Tests mit @Test(expected=Exception.class)
annotieren muss. Warum zum Glück? Weil mit dem Werfen der Exception der Test vorbei war und keine weiteren Assertions im gleichen Test mehr möglich waren. Das kann man zwar mit einem try
/catch
-Konstrukt umgehen, aber der Test wird dann schnell sehr unleserlich. JUnit5 bietet ein assertThrows()
. Das ist allerdings weniger flexibel als die Lösung, die AssertJ offeriert, denn mit catchThrowable()
erhält man dort eine Referenz auf das Throwable und kann nach dem Fluent Interface darauf weitere Assertions anwenden. Der folgende Code verdeutlicht dies.
assertThat(catchThrowable(() -> { throw new RuntimeException("BAMM"); }))
.isNotNull()
.isInstanceOf(RuntimeException.class)
.extracting(Throwable::getMessage)
.isNotNull()
.isEqualTo("BAMM");
- AssertJ-Dokumentation auf GitHub - AssertJ in PwMan3