Fakten, Fakten, Fakten! Midje als alternatives Testframework für Clojure

Midges sind kleine aber unangenehme Zeitgenossen, die einen Aufenthalt in den schottischen Highlands ohne die richtige Vorbereitung zur Hölle machen können. Das Clojure Testframework Midje hat mit diesen Mücken zum Glück nur den Namen gemein. Im Gegensatz zu ihnen macht es das Entwicklerleben nicht schwerer, sondern das Testen von Clojure-Programmen auch ohne große und aufwendige Vorbereitung und Einarbeitung sehr angenehm.

Midje (sprich [mɪdʒ]) ist ein Test-Framework für Clojure und damit eine Alternative zu clojure.test. Große Stärken von Midje sind die bessere Lesbarkeit von Tests und das einfache Stubben und Mocken von Funktionen. Die Tests werden in einer Form geschrieben, die auch in Clojure-Büchern wie zum Beispiel The Joy of Clojure für Beispiele verwendet wird. Eine sehr einfache Testerwartung sieht etwa so aus:

Facts und Checkables

Tests werden anhand von Fakten zusammengefasst und strukturiert. Das Makro fact bzw. facts kann optional um einen auch bei Clojure-Funktionen üblichen Docstring zur genaueren Erläuterung des Tests ergänzt werden. Die Verwendung dieser Dokumentation hat sich als sehr sinnvoll für den schnelleren Überblick über eine Testsuite erwiesen. Facts können beliebig verschachtelt werden, wobei facts lediglich ein Synonym für fact ist, um die Lesbarkeit zu verbessern. (Listing 1)

Listing 1: Facts

Facts bestehen aus einem oder mehreren Checkables. Ein Checkable besteht aus einem Pfeil (=>) und einer rechten und linken Seite. Die linke Seite enthält die auszuführende Funktion mit ihren Argumenten, die rechte Seite das erwartete Ergebnis. Statt => kann auch =not=> verwendet werden, um direkt die Ungleichheit der beiden Seiten des Checkables zu prüfen. In Listing 1 befinden sich auf der rechten Seite der Checkables jeweils einfache Werte. Statt einfacher Werte lassen sich über Checker auch komplexere Bedingungen prüfen. Dazu später mehr. Wenn Facts zur Spezifikation von gewolltem Softwareverhalten verwendet werden, werden diese wie im TDD üblich vor dem eigentlichen Verhalten implementiert. Um einen Fact für ein später gewünschtes Verhalten zu implementieren, ohne die Tests fehlschlagen zu lassen, kann das Makro future-fact verwendet werden. Die Checkables in einem future-fact werden bei der Ausführung der Tests nicht evaluiert. Midje gibt lediglich ein WORK TO DO aus. (Listing 2)

Listing 2: Future Fact

Midje bringt ein eigenes Leiningen-Plugin zum Erzeugen der passenden Ordner- und Dateistruktur sowie zum Ausführen der Tests mit. Dieses muss einmal in der Datei ~/.lein/profiles.clj registriert werden:

Ein neues Projekt mit Unterstützung für Midje kann dann per lein new midje projektname angelegt werden. Das Leiningen-Plugin erzeugt automatisch die in Listing 3 dargestellte Dateistruktur und füllt den Test-Namespace mit einem kleinen Beispiel.

Listing 3: Midje Projekt Dateistruktur

Natürlich kann diese Struktur auch manuell in ein bereits bestehendes Projekt eingepflegt werden. Ein Midje Test Namespace verwendet zumindest den getesteten Namespace und midje.sweet, in dem alle relevanten Funktionen und Makros von Midje deklariert sind. (Listing 4)

Listing 4: Grundstruktur eines Midje Test Namespaces

Im Projektverzeichnis können alle vorhandenen Midje Tests per lein midje ausgeführt werden. Dieser Aufruf hat den Nachteil, dass das Starten der JVM und der Clojure-Laufzeitumgebung etwas Zeit in Anspruch nimmt und man so relativ lange auf die Testergebnisse warten muss.

Startet man die Tests hingegen mit lein midje :autotest, wird für den ersten Start zwar genau so viel Zeit benötigt, aber Midje bleibt dann aktiv und erkennt Änderungen an den Tests oder den getesteten Namespaces und führt diese bei jeder Änderung automatisch erneut in der anfangs gestarteten Laufzeitumgebung aus. Wenn man die Tests aus Listing 1 ausführt, erhält man die Ausgabe in Listing 5 unter „Testausführung 1“. Hier zeigt sich auch der Vorteil der Verwendung eines Docstrings für die Fakten. Das Testprotokoll ist dann gut lesbar. Für die zweite Testausführung in Listing 5 wurde der zweite Fakt absichtlich so angepasst, dass er fehlschlägt, um die entsprechende Ausgabe zu demonstrieren. (Die Erwartung wurde von -2 auf 2 geändert.)

Listing 5: Testergebnis

Auf der rechten Seite eines Checkables können statt einfachen Werten, die gegen das Funktionsergebnis verglichen werden, auch von Midje vordefinierte Funktionen, „Checkers“, verwendet werden. Midje ruft den Checker mit dem Ergebnis der Funktion auf der linken Seite des Checkables als Argument auf. Midje bringt bereits eine große Anzahl von Checkern mit. Im Folgenden betrachten wir einige davon exemplarisch. Weitere Checker sind im Midje-Wiki[3] dokumentiert.

truthy, falsey: Diese Checker können verwendet werden, wenn das Ergebnis einer Funktion true oder nicht nil bzw. false oder nil sein soll, ohne dass es auf die genaue Ausprägung des Ergebnisses ankommt.

anything, irrelevant In manchen Fällen ist ein Test so aufgebaut, dass das Ergebnis der Funktion für den Test nicht relevant ist. Üblicherweise trifft das bei Funktionen mit Seiteneffekten (z.B. Datenbank-Inserts) zu. Dann kann die Funktion ausgeführt werden, ohne dass der Rückgabewert Auswirkungen auf den Test hat (Listing 6). Ein weiteres Anwendungsgebiet ist die Verwendung von Prerequisites, die wir später im Artikel betrachten.

Listing 6: anything Checker

throws: Der throws-Checker verlangt, dass ein Funktionsaufruf eine Exception wirft. (fact (throwing-function) => (throws Exception)) Auch das Prüfen der Exception-Message ist möglich. (fact (throwing-function) => (throws Exception „foo“)) Die Exception Message kann auch gegen einen regulären Ausdruck geprüft werden. (fact (throwing-function) => (throws Exception #“(foo|bar)“))

contains: Dieser Checker kann auf Sequences oder Maps angewendet werden. Er prüft, ob sich ein bestimmter Wert bzw. ein Schlüssel-Wert-Paar darin befindet. Es können beliebig viele Werte bzw. Paare als Argument übergeben werden. Außerdem können statt konkreten Werten auch Funktionen verwendet werden, die Eigenschaften der Werte prüfen. (Listing 7)

Listing 7: contains Checker

Man kann eine Checker-Funktion auch selbst implementieren. Dazu schreibt man eine Higher-Order-Function, die eine Funktion zurückgibt, die als erstes Argument das zu prüfende Funktionsergebnis erwartet. (Listing 8)

Listing 8: Eigener Checker

Stubs und Mocks sind aus der objektorientierten Welt bekannt. Sie werden dort verwendet, um ein Objekt unabhängig von seinen Kollaborateuren testen zu können. In der funktionalen Welt gibt es zwar keine kollaborierenden Objekte aber andere Funktionen, die von der zu testenden Funktion aufgerufen werden. Um die zu testende Funktion unabhängig zu machen, bietet Midje die provided Form. Unter Verwendung von provided lassen sich einerseits vorgefertigte Ergebnisse aus einer Funktion (Prerequisites) zurückgeben und andererseits die Aufrufe der Funktion protokollieren und abprüfen. Nehmen wir an, wir wollen eine Funktion authenticate implementieren, die anhand ihrer Argumente einen User in der Datenbank sucht und dann prüft, ob dessen Passwort korrekt angegeben wurde. Ist das Passwort korrekt, wird der User zurückgegeben, ansonsten nil. Wollen wir diese Funktion ohne Datenbankanbindung testen, so müssen wir die Datenbanksuche so stubben, dass ein passender Testuser zurückgegeben wird. Dazu geben wir zusätzlich zum eigentlichen Checkable eine provided Form an, in der wir den gewünschten Rückgabewert der gestubbten Funktion für unseren Test spezifizieren. (Listing 9)

Listing 9: Stubbing mit der provided Form

Gleichzeitig kann man die provided Form auch zum Mocken verwenden. Der Test in Listing 9 würde auch fehlschlagen, wenn authenticate nicht find-user mit dem in der provided Form angegebenen Argument – dem Usernamen > – aufruft. Ist für das Mocken auch die Aufrufhäufgkeit einer Funktion interessant, kann diese Erwartung ebenfalls ergänzt werden. Im Beispiel soll second-function beim Aufruf von first-function fünf Mal, aber third-function nie ausgeführt werden. Die Rückgabewerte der aufgerufenen Funktionen sind für diesen Test nicht relevant. (Listing 10)

Listing 10: Aufrufhäufigkeiten beim Mocken

Fazit

Midje ist ein mächtiges und zugleich sehr einsteigerfreundliches Framework zum Implementieren von Tests in Clojure. Die damit entstehenden Tests sind leichter zu lesen als die Pendants in clojure.test, was nicht zuletzt der Pfeil-Schreibweise (=>) und den einfachen Stubbing- und Mockingmöglichkeiten zu verdanken ist, die in clojure.test nur durch das Schreiben eigener Makros oder das Einbinden weiterer Bibliotheken zu erreichen sind. Die gute Unterstützung durch das passende Leiningen-Plugin rundet das positive Gesamtbild weiter ab. Midje bietet noch eine Reihe weiterer Möglichkeiten um Tests zu implementieren. Diese fortgeschrittenen Elemente würden den Rahmen dieses Einführungsartikels sprengen. Deshalb sei auf das sehr ausführliche Wiki auf den GitHub-Seiten von Midje verwiesen.


comments powered by Disqus