JVM Testing im Vergleich: JUnit vs Spock vs Kotest

geschrieben von:

Ronny Kissing

Ronny Kissing ist seit 3 Jahren als Backendentwickler bei der Finatix und ist damit ein Mann der ersten Stunde. Ronnys Fokus liegt auf den JVM Sprachen und auf der Entwicklung von Microservices in Spring Boot.

Aus der Rubrik:

Fachbeiträge

Fachbeiträge

james-harrison-vpOeXr5wmR4-unsplash

Die Softwareentwicklung ist in der heutigen Zeit ein komplexes Gebiet. Softwarelösungen werden immer größer und aufwändiger. Auch die Anzahl an Codezeilen steigt stetig. Es werden umfangreiche Architekturen selbst für scheinbar einfache Projekte aufgespannt, denn nur selten reicht eine Softwarekomponente aus. Da kein Code frei von Fehlern ist, steigt damit allerdings auch die Fehleranfälligkeit. Um dem entgegenzuwirken wird regelmäßig getestet. Drei Teststufen haben sich dabei etabliert:

Instagram Square Pyramid Chart - CC (1)
  • System Tests bzw. End2End Tests, welche das Gesamtsystem und das Zusammenspiel mehrerer Komponenten absichern und die erwartete Funktionalität sicherstellen sollen
  • Integrationstests, welche die entwickelte Komponente abdeckt (Bsp.: Spring Integrationtests)
  • Unit Tests als unterste Ebene, welche die Logik und den Code selbst prüfen

Damit Tests einfacher implementiert werden können, gibt es verschiedene Frameworks. Nachfolgend sollen drei dieser Testframeworks für die sehr verbreiteten und beliebten JVM Sprachen[1] näher betrachtet werden. Um den Rahmen der kleinen Analyse nicht zu sprengen, werden an dieser Stelle nur die genannten Frameworks beleuchtet.

Um Code und Tests noch effektiver miteinander zu verbinden, gehen viele sogar so weit, ihre Software nach TDD[2] zu entwickeln. Das funktioniert nur für Unit- und Integrationtests, bietet aber einen großen Mehrwert. Dazu schreibt man als Erstes die Tests um das erwartete Verhalten der zu testenden Funktion zu definieren. Diese Tests lässt man gezielt fehlschlagen, um anschließend die Funktionalität zu implementieren. Bei genauer Arbeit, gelangt man zu einem sehr robusten Code. Auch hier können selbstverständlich nicht alle Fehler ausgeschlossen werden, die Wahrscheinlichkeit ist durch dieses Vorgehen allerdings deutlich geringer.

Zum Schreiben von Tests unterstützen die Testframeworks bzw. Bibliotheken. Dabei zählt JUnit, vor allem in der neusten Version Jupiter, wohl zu den bekanntesten. Das Spock Framework und Kotest sind zwei Alternative, die man in Betracht ziehen kann. Dabei beschränkt sich Kotest auf Kotlin.

Testframework: JUnit

Das JUnit Framework, als bekanntestes und wahrscheinlich ältestes Testframework, gibt es inzwischen in der Version 5 – Jupiter. Bis hierhin gab es viele bemerkenswerte Evolutionsstufen und mit jeder Iteration interessante neue Features und Erweiterungen. Eine davon ist @DisplayName um ein wenig mehr Übersicht zu bekommen. DisplayName kann sowohl für die gesamte Testklasse als auch für einzelnen Tests verwendet werden. Eine ähnliche Vorgehensweise wird in dem Framework Spock genutzt. Auch hier bekommen die Testschritte eine Benennung zugewiesen.

Ebenso gibt es Features wie parameterized Tests, nested Tests oder auch Group Assertions. Welche Jupiter zu einem recht mächtigem Werkzeug machen. Die Tests können in gewohnter Art geschrieben werden.

Was auffällt, ist die Kompaktheit des Tests. Auch erkennt man, zumindest in der IntelliJ IDEA, auf den ersten Blick was in dem Test passieren soll. Durch den Namen des Tests erkennt, ist der Schritt auch lesbar und schnell innerhalb mehrerer Tests zu identifizieren.

Durch den Namen des Tests, der durch den DisplayName zugewiesen wurde, ist dieser schnell und lesbar innerhalb multipler Tests zu identifizieren.

Screenshot-1
Screenshot-2

Wichtig ist auch, Fehlerfälle abzudecken und zu prüfen, ob bestimmte Exceptions geworfen werden. Hierzu bietet Jupiter mit assertThrows die Möglichkeit das Werfen von Exceptions zu testen.

Screenshot-3

Zu den wichtigen Grundfunktionalitäten gehört auch das Verwenden von Mocks. Es kommt im Testalltag immer wieder vor, dass man bestimmte Funktionen mocken muss oder möchte. Beispielsweise die Kommunikation mit anderen Microservices, damit man die Tests auch ohne die Verwendung des anderen Services ausführen kann. Hierzu kann JUnit beispielsweise mit Mockito verwenden werden.

Ein weiterer, oft unterschätzter und eher unbekannter Punkt, ist die parallele Ausführung von Tests. Mit dem Parameter[3]:

junit.jupiter.execution.parallel.config.fixed.parallelism

Screenshot-4

können Tests erheblich schneller ausgeführt werden, je nachdem welche Hardware vorhanden ist. Diese Features nehme ich als Kernfunktionalität und als sehr wichtig wahr. Bei der Überlegung ein anderes Testframework zu nutzen, sind sie das Minimum und müssen somit unterstützt werden.

Testframework: Spock

Spock baut auf JUnit auf und ist somit vollständig kompatibel zu IDEs wie IntelliJ IDEA oder Eclipse.

Um Tests in Spock zu schreiben sind keine weiteren Voraussetzungen nötig. Anders als bei JUnit werden hier keine „normalen“ Tests, sondern Testspezifikationen definiert und geschrieben. Ein weiterer Unterschied ist, dass Spock Tests nicht in Java, sondern in Groovy geschrieben werden. Dennoch können die Tests vollständig mit der IDE debugget werden. Zudem deckt Spock alle Bereiche ab, die auch durch JUnit abgedeckt werden.

JUnit (1)

Die Testspezifikationen in Spock sind verständlich und auch von Nicht-Softwareenwicklern lesbar, da jede geschriebene Testspezifikation mit Labeln selbsterklärend und verständlich gestaltet werden kann. Label gibt es zwar auch für JUnit Tests, die mit @DisplayName für den gesamten Test festgelegt werden können. Allerdings bietet Spock die Möglichkeit Label für jeden einzelnen Schritt im Test anzugeben. Dadurch werden Tests zugänglicher.

Das Schreiben einer Testspezifikation in Spock ist denkbar einfach. Dazu wird unter „/src/tests/“ ein weiter Ordner namens „groovy“ angelegt und kann dort seine Tests platzieren. In diesem Ordner kann man nun die erste Testspezifikation anlegen. Die könnte wie folgt aussehen:

Screenshot-5

In dem Beispiel sieht man, die einfachste Möglichkeit Tests mit Spock zu schreiben. Die gebräuchlichere Variante ist aber eine eher beschreibende Art. In den Tests werden die Blöcke „Blocks“ geschrieben, wobei jeder Block ein eigenes Label bekommt.

Screenshot-6
Screenshot-8
Screenshot-7
Screenshot-9

Auch das Testergebnis unterscheidet sich. Mit Spock wird nahezu alles aufgeschlüsselt.

Screenshot-10
Screenshot-11

Aus den obigen Beispielen lässt sich der Aufbau eines Spocktests erkennen.

1.     given: Ausgangssituation und Vorbedingung

2.    when: Aktion

3.    then: Auswertung des Ergebnis (expect und actual)

Diese Struktur zeigt auch auf, dass Spock sehr an Behaviour Driven Development orientiert ist. Durch diese Schreibweise können die Tests nicht nur von Softwareentwicklern, sondern auch von Product Ownern oder Requirement Engineers gelesen und definiert werden. Vorstellbar ist ein Szenario, in dem ein Requirement Engineer die Schritte given, when, then mit Label vorgibt, sodass ein Entwickler weiß, was er in dem Test implementieren muss.

Sehen wir uns die anderen Punkte, Mock und parallele Ausführung von Tests. Wie JUnit bietet Spock auch die Möglichkeit Klassen zu mocken. Die Verwendung ist ähnlich zu Java. Eine genaue Anleitung hierzu.

Weiterhin existiert ein gutes CheatSheet unter um eine kompakte Übersicht zu haben.

Der wichtigste Punte bei der Verwendung von Mocks, ist es, die Mocks in der Setup Methode der Testspezifikation anzulegen. Für genauere und tiefer gehende Informationen zur Verwendung von Mocks mit Spock, sollte man sich den Cheatsheet genauer ansehen und die Mocks ausprobieren.

Die parallele Ausführung von Tests ist unter Spock ebenso gegeben wie unter JUnit. Über eine Konfigurationsdatei, SpockConfig.groovy, kann sie aktiviert werden[4].

Somit ist eine performante Testausführung auch unter Spock gegeben.

Testframework: Kotest 

Auch Kotest[5] baut auf JUnit auf. Im Gegensatz zu Spock und JUnit wird allerdings in den meisten IDEs ein Plugin benötigt um die Tests ausführen zu können. So gibt es für IntelliJ IDEA ein Kotest Plugin. Ist es installiert, steht der Verwendung von Kotest nichts im Weg.

Dadurch ist Kotest sehr flexibel einsetzbar. Ein weiterer Vorteil ist die Koexistenz mit JUnit. JUnit und Kotest können beide in einem Projekt verwendet werden, was mit Spock nicht möglich ist, da sich die Dependencies von Spock und JUnit in die Quere kommen.

Kotest bietet weitestgehend die selbe Funktionalität wie JUnit, wobei die größten Unterschiede  in der Sprache und Schreibweise der Tests liegen. Kotest kann ausschließlich in Kotlin geschrieben werden, ist dafür aber native Kotlin. Die Schreibweise liegt allerdings näher an JUnit als an Spock. Das sieht man auch im Testergebnis.

Screenshot-13
Screenshot-14

Möchte man in Kotest Mocks verwenden, so bietet sich MockK [6] an, welches eine native Kotlin Mockimplementierung ist. MockK lässt sich einfach schreiben und der Code ist sehr gut lesbar, wie ebenfalls im unteren Beispiel zu sehen.

Screenshot-12

Ein weiterer Punkt, der bereits für JUnit und Spock aufgegriffen wurde, ist die parallele Ausführung von Tests. Das ist, begrenzt, auch mit Kotest möglich. In der Project Level Config kann man die Parallelität festlegen oder folgende Umgebungsvariable setzen: „kotest.framework.parallelism“

Weitere Informationen und Tutorials.

Zusammenfassung

Dieser kurze Abriss sollte keine Einführung in die jeweiligen Testframeworks darstellen. Das Ziel war es lediglich, eine grobe Vergleichbarkeit aufzuzeigen. Welches Framework man verwendet, hängt stark davon ab was man entwickelt und welche Programmiersprache verwendet werden soll. Auch wie groß der Einfluss von Requirement Engineers im Projekt ist, kann Einfluss auf die Wahl haben. Aus meiner persönlichen Sicht würde ich für Kotlin Projekte mit Kotest oder Jupiter arbeiten. Java Projekten gebe ich Jupiter den Vorzug, da mein persönlicher Programmierstil eher nicht BDD ist. Möchte man allerdings ein BDD Projekt aufsetzen und den Einfluss der Requirement Engineers gerecht werden, plädiere ich in Richtung Spock.

Jedes der drei Frameworks hat seine Vor- und Nachteile. Vielleicht hilft es, die Frameworks einfach auszuprobieren, sich eine eigene Meinung dazu zu bilden und dann eine Entscheidung zu treffen.

Quellen:

[1] https://de.statista.com/statistik/daten/studie/678732/umfrage/beliebteste-programmiersprachen-weltweit-laut-pypl-index/ (Stand 06/2021)

[2] TDD = Test Driven Design

[3]     https://junit.org/junit5/docs/snapshot/user-guide/index.html (siehe Beschreibung für 2.19.1. Configuration)

[5]     Kotest.io (Auf dieser Seite finden sich auch Tutorials, um Kotest zu lernen. Kotest steht für Kotlin Test.)

[6]     https://mockk.io/ , https://kotest.io/docs/framework/integrations/mocking.html

Schaubild 1: angelehnt an: https://www.elbisch.ch/2018/04/26/unit-vs-integration-tests/

Schaubild 2: angelehnt an: https://semaphoreci.com/community/tutorials/stubbing-and-mocking-in-java-with-the-spock-testing-framework