AssertJ - Einführung: Unterschied zwischen den Versionen

Aus MimiPedia
(Die Seite wurde neu angelegt: „= Die grundlegende Syntax = Die Struktur von AssertJ-Anweisungen könnte einfacher nicht sein. Jede Assertion in AssertJ folgt dem gleichen Aufbau; umgangspra…“)
 
Zeile 2: Zeile 2:
Die Struktur von AssertJ-Anweisungen könnte einfacher nicht sein.  
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:
Jede Assertion in AssertJ folgt dem gleichen Aufbau; umgangsprachlich läßt sie so ausdrücken:
{{quote|assert that X has property Y}}
{{quote|assert that object X has property Y}}
oder auf Deutsch:
oder auf Deutsch:
{{quote|stelle sicher, daß X die Eigenschaft Y besitzt}}
{{quote|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:     
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:     
{{java|code=Assertions.assertThat(...)}}
{{java|code=Assertions.assertThat(...)}}
Zeile 24: Zeile 24:




= Ausführung =
== Ausführung ==
Geht die Assertion schief, bemüht sich AssertJ um eine möglichst hilfreiche Meldung:
Geht die Assertion schief, bemüht sich AssertJ um eine möglichst hilfreiche Meldung:
  Assertions.assertThat(2+2).isEqualTo(5);
  Assertions.assertThat(2+2).isEqualTo(5);

Version vom 4. April 2021, 12:03 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 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 {{java|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();
     }
 });