AssertJ - Einführung
Die grundlegende Syntax
Die Struktur von AssertJ-Anweisungen könnte einfacher nicht sein. Jede Assertion in AssertJ folgt dem gleichen Aufbau; umgangsprachlich läßt sie so ausdrücken:
assert that object X has property Y
oder auf Deutsch:
stelle sicher, daß das Objekt X die Eigenschaft Y besitzt
Sie besteht also aus zwei Teilen: dem zu prüfenden Objekt X und der Prüfung Y. Jede -- also zumindest fast jede -- Assertion beginnt mit dem Aufruf der statischen Methode:
Assertions.assertThat(...)
Sie hat als einzigen Parameter einen Java-Ausdruck der das zu prüfende Objekt liefert. Der Ausdruck kann so ziemlich alles sein,
typischerweise beschreibt er eine lokale Variable, ein Objekt, oder ein Methoden-Aufruf.
Auf dem damit erzeugten Objekt kann man -- je nach Typ -- eine Reihe von Methoden aufrufen, die die zu prüfende Eigenschaft darstellen.
Betrachten wir mal als Beispiel die Prüfung, ob 2 + 2
tatsächlich 4
ergibt. Dafür schreiben wir zunächst einmal
Assertions.asserThat(2 + 2)
Tippen wir nun einen Punkt ein, bietet Eclipse eine Fülle von Methoden an -- wir möchten auf Gleichheit
testen, wählen für unser Beispiel .isEqualTo()
aus und schreiben
Assertions.asserThat(2 + 2).isEqualTo(4);
Schon ist die Assertion fertig. Abhängig vom Typ des zu prüfenden Ausdrucks im assertThat
-Aufruf
stehen unterschiedliche Methoden zur Überprüfung zur Verfügung.
In den folgenden Abschnitten werden die meistverwendeten vorgestellt.
Ausführung
Geht die Assertion schief, bemüht sich AssertJ um eine möglichst hilfreiche Meldung:
Assertions.assertThat(2+2).isEqualTo(5); org.junit.ComparisonFailure: expected:<[5]> but was:<[4]>
Sollte die Meldung nicht ausreichen, kann man nach dem Aufruf von assertThat
eine Erklärung einfügen indem man
die .as()
-Methode aufruft:
Assertions.assertThat(2 + 2).as("2 + 2").isEqualTo(5); org.junit.ComparisonFailure: [2 + 2] expected:<[5]> but was:<[4]>
Objekte
Gleichheit
Normalerweise vergleicht man Objekte auf inhaltliche Gelichheit über die .equals()
Methode. Das geht in AssertJ mit:
assertThat(x).isEqualsTo(Y)
Vergleicht man enum-Ausprägungen oder möchte sicherstellen -- was einem Vergleich mit == entspricht -- verwendet man
assertThat(x).isSameAs(Y)
Null oder nicht Null
Dafür gibt es zwei Methoden mit naheliegendem Namen:
assertThat(x).isNull() assertThat(x).isNotNull()
Welcher Typ?
Mit
assertThat(X).isInstanceOf(Foo.class);
kann geprüft werden, ob das Objekt eine Instanz der angegebenen Klasse ist.
Boolean und boolean
Für die Boolean-Typen stehen zwei Methoden mit offensichtlichem Verhalten zur Verfügung:
Assertions.assertThat(x).isTrue() Assertions.assertThat(x).isFalse()
Die Methoden funktionieren für beide Boolean-Typen, für Boolean
ohne die Gefahr einer Null-Pointer-Exception.
Optional
Bevor man den Inhalt eines Optional-Objekts prüfen kann, muß man ihn erst auspacken;
ein Job den AssertJ für uns übernimmt und dabei auch darauf achtet, ob statt eines validen Objekts
vielleicht null
übergeben wurde:
Assertions.assertThat(x).contains("foo")
Ist der Inhalt egal, kann man auch direkt die Präsenz oder Abwesenheit eines Wertes abfragen:
Assertions.assertThat(x).isPresent() Assertions.assertThat(x).isNotPresent()
Statt .isNotPresent()
kann auch .empty()
verwendet werden.
Interessiert nur der Typ des enthaltenen Objekts, hilft AssertJ uns auch:
Assertions.assertThat(x).containsInstanceOf(String.class)
Man beachte den Unterschied zu .instanceOf()
, das den Typ von x
prüft,
während containsInstanceOf()
den Typ von x.get()
prüft.
Listen, Arrays, Kollektionen
Besonders hilfreich ist AssertJb ei der Prüfung von Listen aller Art.
Sehr angenehm ist dabei die gleichartige Behandlung von Typen wie Streams, Arrays und Collections wie List und Set
die alle mit den gleichen Methoden getestet werden. Das bedeutet, daß weniger Methoden zu lernen sind und
wenn sich der Typ eines Ausdrucks ändert - z.B. von List
nach Stream
- muß der Test nicht angepaßt werden.
Das Einfachste ist die Suche nach bestimmten Werten:
Assertions.assertThat(liste).contains("x", "y");
Die Prüfung gelingt, wenn die Liste -- gegebenenfalls neben anderen Strings -- die String "x" und "y" an beliebiger Stelle enthält. Daneben gibt es eine ganze Legion von contains-Methoden wie z.B.
containsAnyOf containsExactly containsExactlyInAnyOrder containsNull containsOnly
Analog dazu gibt es negierte Methoden, die mit doesNotContain
beginnen.
Bei simplen Typen wie {{java|String} oder Zahlen funktionieren die contains-Methoden sehr gut. Bei Objekten muß man in die Objekte
hineinschauen und wenn die .equals
-Methode nicht passend implementiert wurde, erhält man unbefriedigende Ergebnisse.
Verwendet man λ-Prädikate kann man anyMatch
und noneMatch
verwenden.
Möchten wir etwa sicherstellen, daß die Liste ein Objekt mit der ID "5" enthält, könnte das so aussehen:
Assertions.assertThat(liste).anyMatch(x -> x.id().equals(5));
Selbstgebaute Test-Bedingungen -- Conditions
Grundsätzlich lassen sich alle Test-Ausdrücke mit den genannten Konstrukten formulieren. Strenggenommen kann man ja jeden Test als
boolean-Ausdruck formulieren und dann auf True oder False prüfen. Das führt jedoch zu unübersichtlichen Tests und zu endlosen
Code-Duplikationen. Wiederkehrende Prüfungen lassen sich mit Hilfe von wiederverwertbaren Conditions
definieren.
Betrachten wir dazu als einfaches Beispiel (der Einfachheit halber ohne getter und setter) eine Person die einen Namen
hat und einer Abteilung zugeordnet ist:
class Person { String name; String abteilung; Person(String name, String abteilung) { this.name = name; this.abteilung = abteilung; } @Override public String toString() { return name; } }
Um zu testen ob eine Person zur Abteilung "X" gehört kann ein Prädikat mit Hilfe der Methode matches()
nutzen:
Assertions.assertThat(person).matches(p -> p.abteilung.equals("X")); Assertions.assertThat(person).matches(p -> p.abteilung.equals("X"), "ist in X");
Funktional sind beide Varianten identisch, die Angabe der Beschreibung im zweiten Falle erleichtert aber die Interpretation
der Fehlermeldung. Um nicht jedesmal das Prädikat und die Beschreibung neu tippen zu müssen, kann man beides zu einer
Condition
zusammenfassen und irgendwo ablegen:
Condition<Person> InAbteilungX = new Condition<>(p -> p.abteilung.equals("X"), "ist in X");
Der Aufruf sieht dann im Unit-Test so aus:
Assertions.assertThat(person).is(InAbteilungX);
Conditions lassen sich mit not()
negieren, mit allOf()
-- das entspricht einer und-Verknüpfung -- und mit
anyOf()
-- das entspricht einer oder-Verknüpfung -- verbinden.
Für die Erzeugung von Conditions kann man auch Factory-Methoden definieren. Hier wird zu einer Abteilung eine Condition erzeugt, die die Zugehörigkeit einer Person zu einer Abteilung testet:
static Condition<Person> inAbteilung(String abt) { Predicate<Person> pred = p -> p.abteilung.equals(abt); String description = "Abteilung " + abt; return new Condition<Person>(pred, description); }
im Unit-Test sieht der Aufruf damit so aus:
Assertions.assertThat(person).is(inAbteilung("X"));
Ist die Condition nicht erfüllt, erhält man folgende Ausgabe. Für die Ausgabe des Namens ist die toString
-Methode
der Klasse Person
verantwortlich:
java.lang.AssertionError: Expecting: <Kasimir> to have: <Abteilung X>
Exceptions
Mit der assertThat
-Methode kann nicht getestet werden, ob bei einem Methoden-Aufruf eine Exception geworfen wird oder nicht.
Das funktioniert deshalb nicht, weil der Ausdruck, der assetThat
als Argument übergeben wird ausgewertet wird bevor
die assertThat
-Methode ausgeführt wird.
Für den Test auf Exceptions wird die Ausführung in einen λ-Ausdrck verpackt, dieser einer geeigneten Methode übergeben und as Ergebnis bei dessen Ausführung analysiert -- wie funktioniert das konkret?
Bei der ersten Variante wird ein λ-Ausdruck übergeben und anschließend die erwartete Exception angegeben.
Im Beispiel wird die Methode foo
des Objekts subject
aufgerufen:
Assertions.assertThatThrownBy(() -> subject.foo()).isInstanceOf(NullPointerException.class);
Die erwartete Exception-Klasse wird von der Methode isInstanceOf()
geprüft.
Die zweite Variante legt erst die erwartete Exeption fest und prüft dann den λ-Ausdruck:
Assertions.assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> subjekt.foo());
Die zweite Variante verstößt gegen das sonst in AssertJ durchgängige Konzept erst den Test-Aufruf und dann das erwartete Ergebnis anzugeben. Es gibt jedoch einige Methode für oft benötigte Exceptions:
Assertions.assertThatIllegalStateException().isThrownBy(() -> subjekt.foo()); Assertions.assertThatIllegalArgumentException().isThrownBy(() -> subjekt.foo()); Assertions.assertThatNullPointerException().isThrownBy(() -> subjekt.foo()); Assertions.assertThatIOException().isThrownBy(() -> subjekt.foo());
Geht das auch ohne λ?
Grundsätzlich sind λ-Ausdrücke und anonyme Klassen austauschbare Konzepte.
Man kann daher λ-Ausdrücke durch Klassen ersetzen die das geeignete (funktionale) Interface implementieren.
Die Methode assertThatThrownBy()
akzeptiert als Parameter ein Objekt das das Interface
ThrowingCallable
implementiert. Daher kann man die erste Variante mit einer anonymen Klasse so schreiben:
Assertions.assertThatThrownBy(new ThrowingCallable() { @Override public void call() throws Throwable { subjekt.foo(); } }).isInstanceOf(NullPointerException.class);
Die zweite Variante sieht dann -- wir verwenden hier die Spezial-Methode für Nullpointer-Exceptions -- so aus:
Assertions.assertThatNullPointerException().isThrownBy(new ThrowingCallable() { @Override public void call() throws Throwable { subjekt.foo(); } });