Plug and Play

Alternative Assertions mit Hamcrest

JUnit bringt für die eigentliche Aufgabe der Prüfungen einen festen Satz von Asserts mit:

Basic Assertions

org.junit.Assert.*
org.junit.jupiter.api.Assertions.*
Aufrufe mit Überschreibungen
assertTrue(condition)
assertFalse(condition)
Für Boolesche Ausdrücke
assertNull(object)
assertNotNull(object)
Für Objekte
assertEquals(expected, actual)
assertNotEquals(expected, actual)
Für Objekte und Primitives
assertSame(expected, actual)
assertNotSame(expected, actual)
Für Objektinstanzen
assertArrayEquals(expected, actual)Für Objekt- und Primitive-Arrays

Diese erlauben alle eine optionalen String mit einer Fehlermeldung auszugeben, wenn das Assert scheitert (Hinweis: In Junit 4 als erster Methodenparameter, in JUnit 5 als letzter Parameter oder in einem Lambda-Ausdruck.) Natürlich läßt sich damit der Großteil aller Prüfungen in einem Test abdecken.

Genaugenommen reicht sogar ein assertTrue() für alles aus, wenn man den Wahrheitswert selbst berechnet – Das wäre dann aber eine Lösung für Leute, die nur einen Hammer für alle Probleme haben, und sich somit auf die Suche nach Nägeln begeben müssen …

Bei der häufigen Arbeit mit Testfällen findet man jedoch viele, die immer wieder auftreten und wiederkehrende Bedingungen erfüllen müssen. Klassische Beispiele sind:

  1. Teilstring-Vergleiche
    – beginnt oder endet eine Zeichenkette mit einer anderen oder enthält sie diese?
  2. Randbedingungen & Annäherungen
    – liegt der Wert außerhalb der Grenzen oder stimmt er nur ungefähr?
  3. Multiple voneinander abhängige Prüfungen
    – mehrere Bedingungen müssen gleichzeitig zutreffen, damit der Test erfüllt ist.

Hinzu kommt, daß wir im Fehlerfall auch eine aussagekräftige Beschreibung haben wollen, was denn nun eigentlich schief gegangen ist.

Eine Lösung für alle diese Aufgaben bietet die Hamcrest-Bibliothek.

Das Matcher-Konzept

Die Prüfung unter Verwendung von Hamcrest läuft über genau ein Statement.

Dies ist jedoch kein Hammer, sondern ein Allzweckwerkzeug mit austauschbarem Prüfelement:

assertThat(actual, matches)

Das matches-Objekt ist hier das, welches die eigentliche Prüfung vollzieht, falls nötig die Informationen über den Verlauf liefert, und ist vom Typ Matcher:.

public interface Matcher<T> extends SelfDescribing {
    boolean matches(Object item);
    void describeMismatch(Object item, Description mismatchDesc);
}

Die aktuelle Bibliothek liefert bereits viele Implementierungen mit, die alle über den statischen import von org.hamcrest.Matchers.* eingebunden werden können.

Dabei beschränkt sich die Anwendung nicht ausschließlich auf den JUnit-Fall mit assertThat( …). Viele andere Bibliotheken verwenden inzwischen das Konzept, wie z.B. Espresso für das Bestimmen von UI-Elementen auf einer Android-Oberfläche. Auch der ErrorCollector, eine JUnit 4 Rule, verwendet in seinen Aufrufen den Matcher:

public <T> void checkThat(final T value, final Matcher<T> matcher)

Die klassischen Fälle

org.hamcrest.Matchers.* 
assertThat(actual, is(…))
assertThat(actual, not(…))
assertThat(actual, anything())
Für Boolesche Ausdrücke
assertThat(actual, nullValue())
assertThat(actual, notNullValue())
Für Objekte
assertThat(actual, equalTo(expected))
assertThat(actual, not(equalTo(expected)))
Für Objekte und Primitives
assertThat(actual, sameInstance(expected))
assertThat(actual, is(theInstance(expected))
assertThat(actual, instanceOf(class))
assertThat(actual, isA(class))
assertThat(actual, is(any(superclass)))
Für Objektinstanzen und Typen
assertThat(actual, hasItem(item))
assertThat(actual, hasItems(item1, item2, …))
Für Objekt- und Primitive-Arrays

Anzumerken wäre, daß es die Matcher is(…) und not(…) hier mehrere Möglichkeiten anbieten die Prüfungen auszuschreiben, indem sie die anderen Matcher schachteln.

Damit braucht nur der Positiv-Fall implementiert zu werden, der durch einfache Negation auch das Gegenteil abdeckt. Des weiteren ist es auch möglich, mehrere Prüfbedingungen zu einem Matcher zusammen zu fassen:

org.hamcrest.Matchers.* 
allOf(A, B, C, …)Alle Bedingungen müssen zutreffen (Und-Verknüpfung)
anyOf(A, B, C, …)Mindestens eine Bedingung muß zutreffen (Oder-Verknüpfung)
both(A).and(B)Beides muß zutreffen  (Und-Verknüpfung)
either(A).and(B)Genau eines muß zutreffen (Exklusiv-Oder)
everyItem(A)Jedes Element einer Liste muß die Bedingung erfüllen
array(A, B, C, …)Für jedes Element einer Liste ist genau eine Bedingung erfüllt.

Hier zeigt sich auch eine weitere Stärke von Hamcrest: Die Namen wurden so gewählt, daß sich aus den geschachtelten Elementen fast ein vollständiger Satz herauslesen läßt. Weiterhin kombinieren sich die Fehlermeldungen ebenfalls zu einem aussagekräftigen Text. Hier ein Beispiel:

assertThat(Double.NaN, is(not(equalTo(Float.NaN))));

Was zur Antwort führt:

java.lang.AssertionError: Values should be different. Actual: NaN

Erweiterung der Möglichkeiten

Zahlreiche weitere Matcher stehen zur Verfügung, unter anderem um die zuvor genannten Fälle abdecken zu können, wie z.B: containsString(…), startsWith(…), endsWith(…), isEmptyOrNullString(…), lessThan(…), greaterThanOrEqualTo(…), isIn(…), isOneOf(…), … und viele, viele mehr.
Und sollte das doch nicht ausreichen, kann man jederzeit weitere Matcher erstellen, die die gewünschten Aufgaben erfüllen.

org.hamcrest.* 
BaseMatcherBasis-Klasse
CustomMatcherfür anonyme innere Klassen
TypeSafeMatcherfür Typsicherheit, Nullcheck und Cast: matchesSafely()