Hör mal wer da testet – Java Edition

Hallo Spaß Coder,

in den letzten Artikeln Testautomatisierung, Von Tests und Komponenten sowie Kein Unit-Test wie jeder andere haben wir euch heiß darauf gemacht, euren Code automatisiert auf funktionale Richtigkeit hin zu überprüfen. In diesem Artikel möchten wir euch ein paar Werkzeuge vorstellen, mit dessen Hilfe ihr euren Java-Code automatisiert testen könnt.

Für Werkzeuge, die ihr im Umfeld von .Net verwenden könnt wird es noch einen eigenen Artikel geben.

Die hier verwendeten Beispiele testen eine Implementierung des Katas Russische Bauernmultiplikation aus dem Fundus der Coding Dojos der CCD.

JUnit

Mit Hilfe von JUnit können wir unseren Code einfach automatisch testen, ohne die Anwendung selbst starten zu müssen. Wir können Test-Code schreiben und darin die Methoden unserer Klasse aufrufen. JUnit führt diese Tests dann aus und bietet auch eine Möglichkeit, die Ergebnisse zu prüfen.

Alle modernen IDEs binden JUnit ein. Die Tests lassen sich mit einem Klick starten und zeigen die Ergebnisse des Testaufrufs komfortabel an. So zeigt in aller Regel ein grüner Balken, dass alle Tests erfolgreich durchlaufen wurden, ein roter, dass Tests fehlgeschlagen sind und ein gelber Balken zeigt an, dass Tests nicht ausgeführt wurden. Bei fehlgeschlagenen Tests sehen wir dann, wo der Test fehlschlägt und wir können uns sofort an die Behebung des Problems machen.

 

Beispiele

Fangen wir mit einem einfachen Test an. Wir wollen testen, ob die Methode Mul 1×1 rechnen kann. Wir erwarten, dass dabei das Ergebnis 1 herauskommt.

Seit JUnit 4 werden Tests durch Annotations gekennzeichnet. So leitet @Test hier die Testmethode ein. Dadurch weiß JUnit, dass dies ein ausführbarer Test ist.
Als nächstes erstellen wir eine Instanz unserer zu testenden Klasse RussischeBauernmultiplikation. Wir haben uns angewöhnt die zu testende Variable sut zu nennen, als Abkürzung für Subject Under Test. Dadurch erkennen wir auch bei komplexeren Testmethoden sofort, welche Klasse was wir eigentlich testen.
In der letzten Zeile wird nun geprüft, ob unsere Behauptung (englisch: Assertion) überprüft, dass 1×1 = 1 ist. Der erste Parameter der Methode assertEquals ist dabei unser erwartetes Ergebnis (1) und der zweite Parameter der Methodenaufruf auf unserer Testklasse.

Funktioniert der Test, zeigt unsere IDE einen grünen Balken an. Schlägt der Test hingegen fehl, wird der Balken rot und JUnit zeigt uns an, welcher Test fehlgeschlagen ist. Hier eine beispielhafte Ausgabe von JUnit, wenn die Methode Mul(1, 1) statt der erwarteten 1 eine 2 zurück gibt:

junit.framework.AssertionFailedError:
Expected :1
Actual     :2
    at de.invidit.unittest.RussischeBauernmultiplikationTest.testOneTimesOneIsOne(RussischeBauernmultiplikationTest.java:12)

Wir sehen also, dass der Aufruf in Zeile 12 der Test-Klasse RussischeBauernmultiplikationTest zu einem Fehler führt.

Auch der Aufbau des Tests, wie wir ihn im Artikel Von Tests und Komponenten beschrieben haben ist hier erkennbar. Hier nochmal eine deutlichere Aufteilung am Beispiel des Tests 2×2 = 4:

Neben der zu testenden Klasse legen wir auch unser erwartetes Ergebnis in einer Variablen ab. Passend zum Parameternamen der assert-Methoden von JUnit nennen wir die Variable expected. Dann rufen wir unsere zu testende Methode auf, also Mul und speichern das Ergebnis in der Variable actual. Zum Schluss prüfen wir die Behauptung (assertion), ob unsere Erwartung (expected) mit dem tatsächlichen (actual) Ergebnis übereinstimmt.

Auf diese Weise lassen sich die meisten Methoden einfach testen.

 

AssertJ

Die Bibliothek AssertJ erweitert JUnit um einen Aspekt, der unsere Unit-Tests lesbarer macht. So liest sich die Überprüfung unserer Behauptung wie wir sie oben formuliert haben ein wenig holprig:

„Behaupte gleich, dass erwartet wie tatsächlich.“

Hä? Wer redet denn so einen Quatsch?

Wäre es nicht schöner, wenn wir unsere Behauptung in etwa so formulieren könnten:

„Behaupte, dass tatsächlich gleich ist mit erwartet.“

Das Klingt zugegebenermaßen immer noch ein wenig holprig. Versuchen wir das Ganze mal in Englisch:

„Assert that actual is equal to expected.“

In unseren Ohren klingt das doch ganz gut. AssertJ hilft uns dabei, unsere Behauptungen genau so zu formulieren.

 

Beispiele

Die beiden Beispiele von oben nun noch einmal mit Hilfe von AssertJ:

Das liest sich doch schon ganz gut. Verwenden wir nun noch die beiden Variablen, mal sehen, wie es dann aussieht:

Das liest sich doch sehr gut.

 

Weitere Features von AssertJ

Neben isEqualTo bietet AssertJ noch eine ganze Menge anderer Methoden um unsere Behauptung zu prüfen. Hier einmal ein Auszug:

  • Strings
    • isEmpty / isNotEmpty
    • contains / doesNotContain
    • startsWith / endsWith
    • hasLineCount
  • Integer
    • isGreaterThan / isLessThan
    • isNotZero
    • isPositive / isNegative
    • isBetween
  • Float
    • isCloseTo
    • isNaN
    • isEqualTo mit Offset
  • Listen
    • isNotEmpty / isEmpty
    • contains / doesNotContain
    • containsOnlyOnce / haveAtLeastOne
    • containsExactly / containsSequence

Darüber hinaus enthält AssertJ einige nützliche Hilfsmethoden um Objekte in Listen zu prüfen:

In diesem Fall wird die Liste persons gefiltert, nach der id mit dem Wert 0. Dann wird von der Person geprüft, ob der name der Person „Hans“ lautet. Es erscheint auf den ersten Blick ein wenig kompliziert, ist aber recht einfach zu verstehen, wenn man sich mal ein wenig damit beschäftigt hat.

Auch für den Umgang mit Daten (von Datum) liefert AssertJ ein paar nützliche Methoden:

Wir sehen, dass wir ohne großen Aufwand unser Datum prüfen können, teils mit recht komplexen Vergleichen, die hier leicht aufrufbar sind.

Was wir in diesem Beispiel zusätzlich sehen ist eine best-practice zum Umgang mit ManagedExceptions in Unit-Tests. Statt einen Try-Catch-Block um den Aufruf von parse zu machen (die Methode wirft eine ParseException), schreiben wir an die Test-Methode einfach throws Exception. Das hält unseren Test-Code lesbar, eine Fehlerbehandlung benötigen wir in der Regel nicht.

 

Mockito

Während wir mit den vorgenannten Werkzeuge grundsätzlich Unit-Tests schreiben können, hilft uns Mockito dabei unsere Tests von ggf. vorhandenen externen Einflüssen zu isolieren. Falls euch jetzt nicht klar sein sollte, was wir damit meinen, lest noch einmal den Artikel Von Tests und Komponenten. Hier wird auf die Notwendigkeit und die unterschiedlichen Möglichkeiten eingegangen.

 

Beispiele

Mockito ist ein sehr mächtiges Werkzeug. In diesem Rahmen möchten wir euch die wichtigsten Anwendungsfälle zur Isolation erläutern, das Stubbing.

Wir rufen eine Methode auf und können mit Hilfe von Mockito festlegen, welchen Wert diese Methode zurückgeben soll. Der folgende Test wird erfolgreich durchlaufen:

Die Methode Mul(1, 3) gibt eigentlich den Wert 3 zurück (1×3 = 3). Wir haben Mockito aber folgendes gesagt:

„Wenn die Methode Mul mit den Parametern 1 und 3 aufgerufen wird, gebe den Wert 5 zurück.“

Damit wir das überhaupt tun können, müssen wir zunächst mit Hilfe von Mockito einen mock der Klasse erstellen, dessen Methodenrückgabe wir verändern möchten. In unserem obigen Fall handelt es sich dabei um ein Interface, es kann aber auch eine Klasse sein.

Wichtig zu wissen ist, dass bei einem Mock immer nur die Methoden einen Wert zurückgeben, die wir auch konfiguriert haben. Ergänzen wir noch folgenden Assert, wird der Test fehlschlagen:

org.junit.ComparisonFailure:
Expected :6
Actual     :0

Die Methode Mul haben wir nur für die Parameter (1, 3) konfiguriert. Rufen wir sie mit (1, 1) auf, wird der Initialwert des Rückgabetyps (in unserem Fall int) zurückgegeben (dieser ist bei int 0).

 

Matcher

Möchten wir Methodenaufrufe mit beliebigen Parametern konfigurieren, müssen wir sog. Matcher verwenden:

Der hier verwendete Matcher heißt anyInt() und sorgt dafür, dass immer dann der Wert 5 zurückgegeben wird, wenn die Methode Mul im ersten Parameter mit irgendeinen Integer aufgerufen wird. Der zweite Wert muss immer 3 sein. Auch hier wird ein Matcher verwendet, er heißt eq(…) und greift dann, wenn der angegebene Wert genau gleich ist.

 

Wichtig: Wird in der Konfiguration bei einem der Parameter ein Matcher verwendet, dann muss auch in allen anderen Parametern ein Matcher verwendet werden!

Folgender Code funktioniert so nicht, da wir den Matcher anyInt() für den ersten Parameter verwenden, aber beim zweiten Parameter keinen Matcher angegeben haben:

Die Fehlermeldung die erscheint lautet:

org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at de.invidit.unittest.ExampleMockTest.testMockitoSimpleWhenThenReturnWithMatcher(ExampleMockTest.java:33)

In manchen Fällen lautet die Meldung bei zwei Parametern 3 matchers expected, 2 recorded, auch wenn beide Parameter als Matcher angegeben wurden. In einem solchen Fall hat es uns geholfen auch den Wert bei thenReturn(…) mit einem Matcher anzugeben.

 

Ein „echtes“ Beispiel

Die oben aufgeführten Beispiele sind natürlich nicht sinnvoll, sie dienen nur dem Zweck, die Funktionsweise von Mockito leicht verständlich zu beschreiben. Wenn wir das sut selbst mocken, testen wir den Code unserer Klasse den wir testen wollen nicht mehr. Hilfreich ist ein Mock aber immer dann, wenn wir unsere Klasse von Abhängigkeiten trennen wollen.

Nehmen wir als Beispiel den Test der Klasse „Würfel“, dessen Methode „würfeln“ einen zufälligen Wert verwendet:

Im Grunde ist die Methode nicht testbar, da hier immer andere Ergebnisse herauskommen, wenn von der Klasse Random die Methode nextInt() aufgerufen wird. Mit Hilfe von Mockito können wir die Methode aber trotzdem testen:

Dieser Test funktioniert. In dem wir festlegen, dass beim Aufruf von nextInt() immer ein fester Wert zurückkommt, können wir testen, ob die Methode roll() die Summe der Einzelwürfe korrekt addiert.

 

Unsere Erfahrung mit den Werkzeugen

Wir setzen alle drei Werkzeuge vor allem im beruflichen Umfeld ein. Hier leisten sie uns gute Dienste und unterstützen dabei, den Code abzusichern. JUnit und AssertJ nutzen wir eigentlich nur noch zusammen, weil der Test-Code dann einfach besser lesbar ist. Mockito hilft bei komplexeren Problemstellungen vor allem dabei, die Tests vom Dateisystem und der Datenbank zu trennen.

 

Zusammenfassung

Mit Hilfe der hier vorgestellten Werkzeuge und Beispiele solltet ihr in der Lage sein, euren Code elegant und automatisiert testen zu können. JUnit bildet die Basis aller Unit-Tests. AssertJ erweitert diese um nützliche Funktionen und sinnvolle, einfach lesbare Syntax. Mockito hilft dabei, Tests von der umliegenden Infrastruktur zu trennen und auch Legacy-Code testbar zu machen ohne gleich die gesamte Code-Basis auf den Kopf stellen zu müssen.

Wir hoffen, dass es euch hiernach leichter fällt, euren sauberen Code auch funktional abzusichern.

Viel Spaß beim Anwenden.

Eure Spaß-Coder

 

Weitere Infos zu den Bibliotheken:

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.