Property-basiertes Testen in Rust: Die Kunst, Bugs zu fangen, die Sie nicht gesucht haben
\n\nHaben Sie jemals Unit-Tests geschrieben, sie ausgeführt, alles war grün, und dann ist die Anwendung bei der ersten ungewöhnlichen Eingabe eines Benutzers abgestürzt? Kommt Ihnen bekannt vor. Klassische Tests sind wie zu prüfen, ob ein Schloss funktioniert, indem man nur den eigenen Schlüssel hineinsteckt. Was aber, wenn ein Angreifer (oder einfach nur der Zufall) einen Schraubenzieher, eine Büroklammer oder ein Stück Draht mitbringt?
\n\nIn Rust, einer Sprache, die für ihre Sicherheit bekannt ist, gibt es ein mächtiges Werkzeug, mit dem man nicht nur bestimmte Szenarien überprüfen, sondern beweisen kann, dass sich der Code für alle möglichen Eingabedaten korrekt verhält. Das ist Property-basiertes Testen. Und heute werden wir es praktisch mit echten Beispielen durchgehen, damit Sie dieses Wissen sofort in Ihren Projekten anwenden können.
\n\nWas ist Property-basiertes Testen und worin unterscheidet es sich vom herkömmlichen Testen?
\n\nStellen Sie sich vor, Sie testen eine Sortierfunktion. Ein gewöhnlicher Unit-Test sieht so aus:
\n\nfn test_sort() {\n let mut arr = vec![3, 1, 2];\n arr.sort();\n assert_eq!(arr, vec![1, 2, 3]);\n}\n\nFunktioniert es? Ja. Aber was, wenn das Array bereits sortiert ist? Was, wenn es leer ist? Was, wenn es 10.000 Elemente in umgekehrter Reihenfolge enthält? Für jeden Fall manuell Tests zu schreiben, ist ein Weg ins Unendliche.
\n\nProperty-basiertes Testen dreht den Ansatz um. Anstatt konkrete Eingabedaten vorzugeben, beschreiben Sie eine Eigenschaft (Property), die für jede korrekte Eingabe gelten muss. Eine spezielle Bibliothek (z. B. proptest oder quickcheck) generiert dann selbstständig hunderte und tausende zufälliger Testfälle und versucht, den einen zu finden, der Ihre Eigenschaft verletzt.
Für die Sortierung könnte die Eigenschaft lauten: „Das Ergebnis der Sortierung hat immer die gleiche Länge wie das ursprüngliche Array, und jedes Element des ursprünglichen Arrays ist genau einmal im Ergebnis enthalten.“ Oder einfacher: „Das Ergebnis ist aufsteigend sortiert.“
\n\nWarum Rust die ideale Umgebung für Property-basiertes Testen ist
\n\nRust fängt bereits viele Fehler zur Kompilierzeit ab (Speicherverwaltung, Datenrennen). Aber es gibt eine Klasse von Bugs, die der Compiler nicht sieht: logische Fehler, fehlerhafte Behandlung von Randfällen, unerwartetes Verhalten bei spezifischen Datenkombinationen. Hier kommt das Property-basierte Testen in Rust ins Spiel.
\n\nEs ergänzt das Typsystem perfekt: Typen garantieren, dass die Daten die richtige Struktur haben, und Property-basierte Tests stellen sicher, dass die Logik zur Verarbeitung dieser Daten für den gesamten Raum möglicher Werte korrekt ist.
\n\nWerkzeuge: proptest vs. quickcheck
\n\nIm Rust-Ökosystem gibt es zwei Haupt-Frameworks: proptest und quickcheck. Beide tun im Wesentlichen dasselbe, aber mit unterschiedlicher Philosophie.
\n\n- \n
- quickcheck ist älter und minimalistischer. Es arbeitet über das
Arbitrary-Trait. Sie schreiben eine Funktion, die zufällige Daten entgegennimmt, und der Test wird mehrfach ausgeführt. \n - proptest ist modern, leistungsstark und bietet eine Vielzahl von Strategien zur Datengenerierung. Es erlaubt eine feine Steuerung, welche Daten generiert werden, und kann ein fehlschlagendes Beispiel auf den minimal reproduzierbaren Fall „verkleinern“ (shrink). Es ist der heutige Goldstandard. \n
Wir werden proptest verwenden, da es mehr Kontrolle bietet und besser dokumentiert ist. Installieren wir es:
\n\n# Cargo.toml\n[dev-dependencies]\nproptest = "1.4"\n\nUnser ersten Property-basierten Test in Rust schreiben
\n\nKehren wir zu unserer Sortierung zurück. Schreiben wir einen Test, der prüft, ob das Ergebnis tatsächlich sortiert ist:
\n\nuse proptest::prelude::*;\n\nfn is_sorted<T: Ord>(slice: &[T]) -> bool {\n slice.windows(2).all(|w| w[0] <= w[1])\n}\n\nproptest!