AssertJ - Einführung: Unterschied zwischen den Versionen

Aus MimiPedia
Keine Bearbeitungszusammenfassung
K (Ullrich verschob die Seite AssertJ Einführung nach AssertJ - Einführung, ohne dabei eine Weiterleitung anzulegen)
 
(kein Unterschied)

Aktuelle Version vom 17. Juli 2021, 14:07 Uhr

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 X has property Y

oder auf Deutsch:

stelle sicher, daß X die Eigenschaft Y besitzt

Sie besteht also aus zwei Teilen: dem zu prüfenden Objekt X und der Prüfung Y. Nahezu jede Assertion beginnt mit dem Aufruf der selben statischen Methode:

Assertions.assertThat(...)

Das hat einen unschätzbaren Vorteil gegenüber JUnit-Assertion, denn man braucht sich für den Einstieg nur eine einzige Methode zu merken. Bei der Auswahl der Test-Methoden hilft die IDE via Code-Completion.

Die assertThat-Methode 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 einen Methoden-Aufruf. Auf dem dambei 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 die IDE 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. Aus der Fülle der Prüf-Methdoen werden in den folgenden Abschnitten einige der 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).isEqualTo(Y)

Vergleicht man enum-Ausprägungen oder möchte sicherstellen daß zwei Objekte identisch sind -- 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?

Um zu prüfen ob das Objekt Instanz einer Klasse ist oder ncht prüft man mit

assertThat(X).isInstanceOf(Foo.class);
assertThat(X).isNotInstanceOf(Foo.class)

Diese Bedinung ist auch dann erfüllt, wenn X einer Unterklasse von Foo angehört (oder nicht). Möchte man sichergehen, daß exakt gegen Foo geprüft wird, verwendet man stattdessen

assertThat(X).isExactlyInstanceOf(Foo.class);
assertThat(X).isNotExactlyInstanceOf(Foo.class)

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.

Der Eierlegende Wollmilch-Test

Nahezu jede Test-Situation für die man nicht sofort eine passende Lösungin AssertJ findet, läßt sich mit einem λ-Ausdruck bewältigen der en Test beschreibt. Nicht zuletzt, weil man hier beliebige Funktionen aufrufen kann die jeden erdenklichen Test ausführen.

Das ist hilfreich, wenn man feststeckt und weitermachen möchte. Man sollte jedoch erwägen -- zu einem späteren Zeitpunkt -- nachzuforschen ob's nicht eleganter geht. Die magische Methode heißt matches und nimmt ein Prädikat vom passenden Typ als Parameter:

assertThat("foo").matches((String s) -> s.trim().length() == 3);

Hier in einem anschaulichen, wenn auch einigermaßen sinnfreien Beispiel.

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

Mehrere Assertions in einem Test

Eine Grundregel für Unit-Tests lautet: Eine Assertion je Test. Warum sollte man also mehrere Assertions in einem Test ausführen? Möchte man nach Ausführung eins Tests z.B. prüfen ob ein Adreß-Objekt korrekt befüllt ist, ist es in der Regel einfacher Alle Felder in einem Test abzufragen. Bisweilen hat man auch komplexere Situationen zu prüfen, etwa wenn ein Objekt-Geflecht erzeugt wird und die Struktur geprüft wird. Dann möchte man nicht einfach die Meldung bekommen "geht nicht" sondern genauere Information darüber was nicht stimmt.

Das Problem bei der Test-Ausführung besteht nun darin, daß das Test-Framework -- in der Regeln JUnit -- nach dem Auftreten des ersten Fehlers den Test abbricht. Man führt den Test dann wieder und wieder aus und fixt jedesmal einen Fehler.

AssertJ bietet -- mit den Soft-Assertions -- die Möglichkeit, die Assertion-Ergebnisse zu sammeln und am Ende der Test-Ausführung gesammelt und komplett auszuwerten. Es giebt zwei Möglichkeiten die Soft-Assertions anzuwenden. Hier zunächst die konventionelle Variante:

SoftAssertions bunch = new SoftAssertions();
    bunch.assertThat(adresse.getStrasse()).isEqulaTo("Alexanderplatz");
    bunch.assertThat(adresse.getHausnummer()).isEqulaTo(15);
    bunch.assertThat(adresse.getPostfach()).isNull();
    bunch.assertAll();

Die Assertions werden in einem {{java|SoftAssertions}-Objekt gesammelt und im Anschluß an die Ausführung durch den Aufruf von assertAll() gesammelt ausgewertet. Der Knackpunkt bei dieser Variante ist, daß der Aufruf leicht vergessen werden kann. Dann wird im Unit-Test -- unbemerkt -- nichts geprüft!

Dieses Problem kann man mit der λ-Variante umgehen:

SoftAssertions.assertSoftly(bunch -> {
    bunch.assertThat(adresse.getStrasse()).isEqulaTo("Alexanderplatz");
    bunch.assertThat(adresse.getHausnummer()).isEqulaTo(15);
    bunch.assertThat(adresse.getPostfach()).isNull();
));

Anstelle der unmittelbaren Ausführung wird der Block nun in einen λ-Ausdruck verpackt. Das {{java|SoftAssertions}-Objekt wird von der statischen Methode assertSoftly erzeugt, mit dem λ-Ausdruck befüllt und assertAll() wird am Ende automatisch ausgeführt.

Der Block des λ-Ausdrucks kann zudem alle beliebigen Java-Anweisungen enthalten, wie zusätzliche Berechnungen, lokale Variablen etcetera.