Warum normale Tests lügen und du ihnen glaubst
Stell dir vor: Du hast eine Funktion geschrieben und mit drei Unit-Tests abgedeckt. Grüner Haken. CI läuft. Produktion – ein Fehler. Kommt dir bekannt vor?
Das Problem klassischer Unit-Tests ist, dass sie konkrete Beispiele überprüfen. Du denkst dir die Eingabedaten aus und schreibst selbst das erwartete Ergebnis. Das ist wie eine Prüfung, bei der der Student seine eigenen Fragen stellt. Früher oder später übersiehst du einen Grenzfall – null, eine leere Liste, eine negative Zahl oder einen String mit Unicode-Zeichen.
Hier kommt das Property-basierte Testen (oder eigenschaftsbasierte Testen) ins Spiel. Anstatt zu schreiben: „Wenn man 5 übergibt, muss 25 zurückkommen“, formulierst du eine Eigenschaft: „Für jede ganze Zahl ist das Quadrat immer nicht-negativ“. Und das Framework generiert selbstständig hunderte zufällige Werte und überprüft diese Eigenschaft.
In diesem Artikel schauen wir uns an, wie man Property-basiertes Testen in Kotlin mit der Bibliothek Kotest einsetzt. Du erfährst, wie du zuverlässige Tests schreibst, Fehler frühzeitig findest und keine Angst mehr vor Refactoring hast. Das ist keine langweilige Theorie – wir schreiben echten Code, den du sofort in deinem Projekt anwenden kannst.
Was ist Property-basiertes Testen und warum Kotlin?
Die Idee ist nicht neu – sie wurde bereits 1999 mit der Bibliothek QuickCheck für Haskell populär. Aber erst jetzt, mit der wachsenden Komplexität von Backend-Systemen und Microservices, wird Property-basiertes Testen zum Must-have im Entwickler-Toolkit.
Kotlin ist die ideale Sprache für diesen Ansatz. Warum?
- Funktionale Möglichkeiten: Lambdas, Extension-Funktionen, Unveränderlichkeit – all das vereinfacht das Schreiben von sauberen Eigenschaften.
- Kotest – ein leistungsstarkes Framework, das von Haus aus Property-basierte Tests, Generatoren und Shrinking (Algorithmus zur Minimierung des fehlschlagenden Beispiels) unterstützt.
- Kompatibilität mit Java: Du kannst Legacy-Code, der in Java geschrieben ist, mit denselben Ansätzen testen.
Kommen wir direkt zur Sache. Unten ist ein Beispiel, das deine Vorstellung vom Testen verändern wird.
Der erste Property-basierte Test in 5 Minuten
Angenommen, wir haben eine Funktion, die prüft, ob ein String ein Palindrom ist:
fun isPalindrome(s: String): Boolean { val cleaned = s.filter { it.isLetterOrDigit() }.lowercase() return cleaned == cleaned.reversed()}Ein normaler Test sieht so aus:
@Testfun `test known palindromes`() { assertTrue(isPalindrome("A man, a plan, a canal: Panama")) assertTrue(isPalindrome("racecar")) assertFalse(isPalindrome("hello"))}Alles funktioniert. Aber was ist, wenn die Eingabedaten Leerzeichen, Ziffern oder Sonderzeichen enthalten? Oder wenn der String leer ist? Oder nur aus Leerzeichen besteht?
Jetzt schreiben wir einen Property-basierten Test mit Kotest:
import io.kotest.core.spec.style.StringSpecimport io.kotest.property.forAllimport io.kotest.property.arbitrary.string
class PalindromePropertyTest : StringSpec({ "Jeder bereinigte String muss für sich selbst ein Palindrom sein" { forAll<String> { original -> val cleaned = original.filter { it.isLetterOrDigit() }.lowercase() isPalindrome(cleaned) == true } }})Was passiert hier? forAll<String> generiert zufällige Strings: leere, mit Leerzeichen, mit Unicode, mit Ziffern. Und für jeden wird die Eigenschaft überprüft. Wenn auch nur ein String die Behauptung verletzt – schlägt der Test fehl, und Kotest zeigt das minimale reproduzierbare Beispiel.
Bereits in dieser Phase kannst du Fehler finden. Wenn der String zum Beispiel nur aus Leerzeichen besteht – gibt filter einen leeren String zurück, und isPalindrome("") gibt true zurück. Ein normaler Test hätte das übersehen.