Blog

Eigenschaftsbasiertes Testen mit Kotlin und Kotest: die Weihnachtsausgabe

Bjorn van der Laan

Aktualisiert Oktober 16, 2025
7 Minuten

Dieser Artikel wurde bereits als Teil des Java-Adventskalenders 2022 veröffentlicht und kann unter PROPERTY-BASED TESTING WITH KOTLIN AND KOTEST nachgelesen werden.

Weihnachten steht vor der Tür, und der Weihnachtsmann und die Elfen arbeiten rund um die Uhr, um alle Spielzeuge zu bauen. Nach einem langen Tag in der Spielzeugwerkstatt wollen die Elfen ihre hart verdienten NorthPoleCoins für Jinglesaft und Süßigkeiten in der örtlichen Kneipe ausgeben. Sie gehen immer in Gruppen und bestellen oft gemeinsam. Sie verwenden eine spezielle Anwendung, um den Überblick über die Zahlungen zu behalten, damit jeder Elf seinen gerechten Anteil bezahlt. Wir sehen den Dienst, der diese Zahlungen abrechnet, weiter unten.

Kotlin und Kotest

Um den Frieden am Nordpol zu wahren, möchten wir einige Tests hinzufügen, um sicherzustellen, dass dieser Dienst wie vorgesehen funktioniert. Eine entscheidende Eigenschaft der obigen Methode ist, dass sich die Salden aller Elfen zu jedem Zeitpunkt zu Null summieren. Andernfalls würden Münzen erzeugt werden oder verloren gehen. Ein Unit-Test für diese Methode könnte etwa so aussehen wie der folgende.

Kotlin und Kotest

Zunächst wird das Szenario eingerichtet, indem die Objekte mit Beispielwerten initialisiert werden. Dann rufen wir die zu testende Methode auf und schreiben Assertions, um zu überprüfen, ob die Ausgabe den Erwartungen entspricht. Natürlich bleibt es nicht bei diesem einen Testfall. Wir schreiben in der Regel weitere, ähnliche Tests, um alle Szenarien und Randfälle ausreichend abzudecken. Und wenn wir sehen, dass alle Testszenarien, die wir uns ausgedacht haben, erfolgreich sind, können wir unseren Code beruhigt festschreiben oder einsetzen.

Kotlin und Kotest

Wenn wir einen Schritt zurücktreten, sehen wir, dass viele dieser Testfälle recht ähnlich sind, nur dass jeder ein etwas anderes Beispiel verwendet. Dieser Ansatz hat ein paar Nachteile. Zunächst einmal müssen die Entwickler mehr Zeit für die Entwicklung und Pflege dieser Testfälle aufwenden. Dazu gehört auch das Ausdenken realistischer Beispiele, was ein gründliches Verständnis der Domäne erfordert. Neuen Entwicklern im Team fällt es möglicherweise auch schwer, die eigentliche Absicht des Tests zu erfassen. Es braucht Zeit, um zu erkennen, welche Aspekte bei jeder Methode abgedeckt sind und was fehlt oder überflüssig ist. Und trotz all dieser Bemühungen des Teams wird nur ein kleiner Teil aller möglichen Beispiele durch die von ihnen geschriebenen Tests abgedeckt. Es ist einfach nicht machbar, alle Kombinationen von Eingaben abzudecken. Und was noch schlimmer ist, die abgedeckten Szenarien sind bei jedem Durchlauf dieselben: im Wesentlichen eine Kopplung zwischen Szenario und Testsuite.

Wenn wir unsere Methoden implementieren, möchten wir, dass die Logik generisch und nicht an bestimmte Fälle gebunden ist. Der Code sollte bestimmte Eigenschaften für alle möglichen Eingaben haben. Die Tests, die wir dafür schreiben, sind eher das Gegenteil: Wir schreiben Tests, die beispielspezifisch und nicht generisch sind. Der Code und die Tests werden auf verschiedenen Abstraktionsebenen entwickelt, wobei Eigenschaften abstrakter sind als konkrete Beispiele.

Eigenschaftsbasierte Tests zur Rettung

Aber was wäre, wenn wir Tests so schreiben könnten, wie wir unseren Code schreiben? Das eigenschaftsbasierte Testen (kurz: PBT) kann uns hier weiterhelfen. Anstatt die konkreten Beispiele zu definieren, die getestet werden sollen, geben Sie die Domäne an (Strings, positive Ganzzahlen usw.) und lassen das Framework viele Eingabewerte generieren, einschließlich Randfällen. Jeder Test wird während eines Testlaufs mehrfach mit verschiedenen möglichen Eingaben ausgeführt. Mit dem eigenschaftsbasierten Testen können wir einen Test schreiben, der alle gewünschten Eingaben testet, einschließlich typischer Randfälle. Dies ermöglicht eine effizientere Erstellung von Tests und gewährleistet, dass ein Test für alle möglichen Werte innerhalb der Domäne erfolgreich ist.

Unten sehen wir, wie unser Test als eigenschaftsbasierter Test aussehen könnte. Die Funktion scenario wird durch die Funktion checkAll von Kotest ersetzt, die als Eingaben einen Satz von Generatoren annimmt. Die Klasse, die alle Generatoren enthält, wird Arb genannt, eine Abkürzung für arbitrary. Für jeden Testlauf wird eine vordefinierte Anzahl von Beispielen generiert, in Kotest standardmäßig tausend. Der Rest des Tests bleibt komplett gleich, außer dass wir etwas Code hinzugefügt haben, um die generierten Werte in die Entitäten und Werte unserer Domäne zu transformieren.

Kotlin und Kotest

Unsere beispielbasierten Tests haben bisher alle bestanden, was uns zu der Annahme verleiten könnte, dass auch dieser eigenschaftsbasierte Test erfolgreich sein wird. Unsere Testfälle repräsentieren alle möglichen Pfade, richtig? Doch dieses Mal sind wir tatsächlich mit einem Fehler konfrontiert. Man könnte meinen, dass ein eigenschaftsbasierter Test aufgrund seiner generierten Eingabewerte schwieriger zu debuggen ist, aber wir haben nicht über die Superkraft von PBT gesprochen. Kotest erwähnt nicht nur das fehlgeschlagene Beispiel, wenn der Test fehlschlägt, sondern beginnt auch, die Eingabewerte zu 'schrumpfen'. Wenn der Test mit dem Integer-Wert 8 als Eingabe fehlschlägt, wird dieser Wert auf den Wert 7 verkleinert und der Test wird erneut versucht. Wenn der Test bei der Eingabe einer Liste mit drei ganzzahligen Zeichenfolgen fehlgeschlagen ist, wird die Liste auf zwei Zeichenfolgen verkleinert. Die Verkleinerung wird so lange fortgesetzt, bis der Test wieder erfolgreich ist. Ziel ist es, das kleinste reproduzierbare Beispiel zu finden, bei dem der Test fehlschlägt, um den Fehler zu finden. Werfen wir einen Blick auf den Stack-Trace unseres Laufs.

Kotlin und Kotest

Der Test schlug fehl, als wir versuchten, die (gerundeten) 77,93 NorthPoleCoins auf vier Elfen aufzuteilen, und Kotest begann, beide Eingaben zu verkleinern. Interessanterweise entdeckten wir bei beiden Verkleinerungen einen anderen Fehler. Das Teilen der Münzen führte zu einem Rundungsfehler, während eine leere Liste eine Ausnahme auslöste. Daran haben wir vielleicht nicht gedacht, als wir unsere Beispiele erstellt haben, aber jetzt können wir unseren Code auf der Grundlage dieser Ergebnisse anpassen.

Wenn wir uns als Entwickler die Verantwortung für realistische Beispiele aufbürden, laufen wir Gefahr, dass solche Fehler durch die Maschen schlüpfen. Eigenschaftsbasierte Tests sorgen dafür, dass alle Szenarien verifiziert werden können.

Benutzerdefinierte Generatoren

Der eigenschaftsbasierte Test, den wir oben gesehen haben, ist aufgrund der Logik, die wir hinzugefügt haben, um die Werte aus den eingebauten Arb-Generatoren so umzuwandeln, dass sie für unseren Test geeignet sind, weniger lesbar, als wir es gerne hätten. Kotest ermöglicht es uns, benutzerdefinierte Arb's über den arbitrary builder zu definieren, und wir können die Erweiterungsfunktionen von Kotlin verwenden, um diese benutzerdefinierten Generatoren auf dieselbe Weise wie ihre eingebauten Gegenstücke zu nutzen.

Kotlin und Kotest

Während Kotest weiß, wie man eingebaute Generatoren schrumpfen kann, kann das Schrumpfverhalten für benutzerdefinierte Generatoren sehr von Ihrer Domäne abhängig sein. Daher können wir mit dem builder eine Shrink-Funktion bereitstellen, die eine Liste von Werten zurückgibt, die wir als Shrink-Ergebnis des aktuell generierten Wertes ansehen. Schauen wir uns einmal an, wie das aussehen könnte:

Kotlin und Kotest

Am Ende hatten wir einen prägnanteren Test als den beispielbasierten Test, mit dem wir begonnen hatten, aber er umfasste viel mehr Beispiele als zuvor.

Kotlin und Kotest

In diesem Artikel haben wir gesehen, was eigenschaftsbasiertes Testen ist und wie es sich von dem häufigeren beispielbasierten Testen unterscheidet. Während die Definition repräsentativer Beispiele beim beispielbasierten Testen in der Verantwortung des Programmierers liegt, überlässt das eigenschaftsbasierte Testen dies den Generatoren, damit die Tests nicht teilweise von den gewählten Beispielen abhängig werden. Das eigenschaftsbasierte Testen ermöglicht es uns, konkrete Beispiele zu verallgemeinern und uns auf die Merkmale des Codes zu konzentrieren, die diesen Beispielen zugrunde liegen. Das Ergebnis ist eine sauberere und kompaktere Testsuite, die einfach zu pflegen ist und subtile Fehler besser aufdeckt. Auf diese Weise bringen wir das, was wir testen, näher an das heran, was wir zu testen behaupten. Es ist jedoch auch wichtig, darauf hinzuweisen, dass das eigenschaftsbasierte Testen nicht dazu gedacht ist, beispielbasierte Unit-Tests vollständig zu ersetzen. Es ist eine Ergänzung zu unserem Testarsenal, die es uns ermöglicht, alle möglichen Eingabewerte mit einem einzigen Test abzudecken, um Fehler aufzudecken, auf die wir selbst vielleicht nicht gekommen wären.

Verfasst von

Bjorn van der Laan

Software engineer at Xebia Software Development

Contact

Let’s discuss how we can support your journey.