Proptest für Rust: Vollständiges Tutorial mit Beispielen

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Was ist Proptest und warum wird es benötigt?

Proptest ist eine leistungsstarke Bibliothek für Property-basiertes Testen in Rust. Im Gegensatz zum traditionellen Unit-Testing, bei dem Sie manuell konkrete Eingabedaten und erwartete Ergebnisse vorgeben, ermöglicht Property-basiertes Testen die Beschreibung von Eigenschaften, die Ihr Programm für alle korrekten Eingabedaten erfüllen muss. Die Bibliothek generiert automatisch eine Vielzahl zufälliger Testfälle und prüft, ob die definierten Eigenschaften eingehalten werden.

Warum ist das sinnvoll?

  • Aufdecken versteckter Fehler: Zufällige Daten finden oft Grenzfälle (Edge Cases), an die der Entwickler gar nicht gedacht hat.
  • Weniger Testcode: Statt Dutzender Beispiele schreiben Sie eine einzige Eigenschaft.
  • Automatische Minimierung (Shrinking): Wenn ein Test fehlschlägt, versucht Proptest automatisch, die minimalen Eingabedaten zu finden, die den Fehler reproduzieren.
  • Dokumentation der Spezifikation: Eigenschaften sind formale, überprüfbare Aussagen über das Verhalten Ihres Codes.

Proptest ist von der Bibliothek QuickCheck aus Haskell inspiriert, wurde aber an die Besonderheiten von Rust angepasst, einschließlich der Arbeit mit Ownership, Borrowing und Traits.

Installation

Proptest ist eine Rust-Bibliothek und wird über den Paketmanager Cargo installiert. Fügen Sie sie im Abschnitt [dev-dependencies] Ihrer Cargo.toml hinzu:

[dev-dependencies]
proptest = "1.4"

Führen Sie anschließend cargo build oder cargo test aus, um die Bibliothek herunterzuladen und zu kompilieren. Proptest wird nur während des Testens verwendet und erhöht daher nicht die Größe Ihrer finalen Binärdatei.

Für asynchronen Code (tokio, async-std) kann eine zusätzliche Konfiguration erforderlich sein, in den meisten Fällen reicht die Standardinstallation jedoch aus.

Schnellstart mit Codebeispiel

Schreiben wir einen einfachen Test, der prüft, ob die Funktion reverse (Zeichenkette umkehren) eine Involution ist: Zweimaliges Anwenden liefert die ursprüngliche Zeichenkette zurück.

// src/lib.rs
pub fn reverse(s: &str) -> String {
    s.chars().rev().collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn test_reverse_involution(s in ".*") { // ".*" — beliebige Zeichenkette
            let reversed = reverse(&s);
            let double_reversed = reverse(&reversed);
            prop_assert_eq!(s, double_reversed);
        }
    }
}

Führen Sie die Tests aus: cargo test. Proptest generiert 256 zufällige Zeichenketten (Standardwert) und prüft, ob die Eigenschaft erfüllt ist. Wenn ein Test fehlschlägt, sehen Sie eine Meldung mit dem minimalen reproduzierbaren Beispiel.

Wichtige Methoden und Funktionen

Nachfolgend finden Sie die wichtigsten Komponenten von Proptest. Jede wird mit Beschreibung, Parametern und Beispiel erläutert.

1. proptest! — Makro zur Definition von Tests

Beschreibung: Das Hauptmakro, das Property-basierte Tests ausführt. Es akzeptiert einen Block mit Testfunktionen und generiert automatisch Eingabedaten.

Parameter: Innerhalb des Makros definieren Sie Tests mit dem Attribut #[test]. Die Eingabedaten werden als param in strategy angegeben.

Beispiel:

proptest! {
    #[test]
    fn test_addition_commutative(a in 0..100i32, b in 0..100i32) {
        prop_assert_eq!(a + b, b + a);
    }
}

2. prop_assert! und prop_assert_eq! — Assertions

Beschreibung: Entsprechungen der standardmäßigen assert! und assert_eq!, aber für Proptest optimiert. Sie unterbrechen den Test nicht sofort, sondern sammeln Fehler und ermöglichen das Shrinking.

Parameter: Wie bei den Standard-Assertions: prop_assert!(Bedingung) und prop_assert_eq!(links, rechts).

Beispiel:

proptest! {
    #[test]
    fn test_length_after_reverse(s in ".*") {
        let reversed = reverse(&s);
        prop_assert_eq!(s.len(), reversed.len());
    }
}

3. any::() — Generierung beliebiger Werte

Beschreibung: Eine Strategie, die einen beliebigen Wert des Typs T generiert, sofern T das Trait Arbitrary implementiert. Nützlich für einfache Typen wie i32, String oder Vec.

Parameter: Keine direkten Parameter; der Typ wird durch die Typinferenz bestimmt.

Beispiel:

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_sorting_identity(mut vec in any::>()) {
        vec.sort();
        prop_assert!(vec.windows(2).all(|w| w[0] <= w[1]));
    }
}

4. Bereichsstrategien (0..100, 1..=10)

Beschreibung: Erzeugen zufällige Werte innerhalb eines bestimmten Bereichs. Unterstützt inklusive (..=) und exklusive (..) Grenzen.

Parameter: Start- und Endwert des Bereichs.

Beispiel:

proptest! {
    #[test]
    fn test_square_non_negative(x in -1000..1000i32) {
        prop_assert!(x * x >= 0);
    }
}

5. prop_oneof! — Auswahl aus mehreren Strategien

Beschreibung: Wählt zufällig eine der angegebenen Strategien aus. Nützlich, um verschiedene Arten von Eingabedaten zu mischen.

Parameter: Eine Liste von Strategien, getrennt durch Kommas.

Beispiel:

use proptest::prelude::*;

fn strategy() -> impl Strategy {
    prop_oneof![
        "fixed".to_owned(),
        any::(),
        "[a-z]{1,10}".to_owned(),
    ]
}

proptest! {
    #[test]
    fn test_with_mixed_strings(s in strategy()) {
        prop_assert!(s.len() <= 10 || s == "fixed");
    }
}

6. prop_compose! — Zusammengesetzte Strategien

Beschreibung: Erstellt benutzerdefinierte, zusammengesetzte Strategien aus bestehenden. Ideal für komplexe Datenstrukturen.

Parameter: Funktionsname, Parameter und Körper, der die Strategie definiert.

Beispiel:

use proptest::prelude::*;

#[derive(Debug, Clone)]
struct Point {
    x: i32,
    y: i32,
}

prop_compose! {
    fn any_point()(x in -1000..1000i32, y in -1000..1000i32) -> Point {
        Point { x, y }
    }
}

proptest! {
    #[test]
    fn test_point_distance(p in any_point()) {
        let distance_squared = p.x * p.x + p.y * p.y;
        prop_assert!(distance_squared >= 0);
    }
}

7. prop_filter! — Filtern von generierten Werten

Beschreibung: Wendet einen Filter auf generierte Werte an. Nur Werte, die die Bedingung erfüllen, werden für den Test verwendet.

Parameter: Eine Strategie und ein Closure, das einen booleschen Wert zurückgibt.

Beispiel:

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_even_numbers_only(x in (0..100i32).prop_filter("nur gerade Zahlen", |x| x % 2 == 0)) {
        prop_assert!(x % 2 == 0);
    }
}

8. prop_assert_ne! — Ungleichheits-Assertion

Beschreibung: Prüft, dass zwei Werte nicht gleich sind. Analog zu assert_ne!.

Parameter: Zwei Werte, die verglichen werden sollen.

Beispiel:

proptest! {
    #[test]
    fn test_reverse_not_identity(s in ".+") { // Mindestens ein Zeichen
        let reversed = reverse(&s);
        prop_assert_ne!(s, reversed);
    }
}

Konfiguration und erweiterte Optionen

Proptest bietet zahlreiche Konfigurationsmöglichkeiten, um das Testen an Ihre Bedürfnisse anzupassen:

  • Anzahl der Testfälle: Mit #![proptest_config = ProptestConfig::with_cases(1000)] können Sie die Anzahl der generierten Testfälle erhöhen oder verringern.
  • Shrinking deaktivieren: ProptestConfig { failure_persistence: None, .. ProptestConfig::default() } deaktiviert die Fehlerminimierung.
  • Timeout: Mit ProptestConfig { timeout: 1000, .. } können Sie ein Zeitlimit pro Testfall setzen.
  • Maximale Schachtelungstiefe: Für rekursive Strategien kann die Tiefe mit ProptestConfig { max_shrink_iters: 10000, .. } begrenzt werden.

Ein vollständig konfiguriertes Beispiel:

use proptest::prelude::*;
use proptest::test_runner::Config;

proptest! {
    #![proptest_config = ProptestConfig {
        cases: 500,
        failure_persistence: None,
        .. ProptestConfig::default()
    }]
    #[test]
    fn test_heavy_computation(x in 0..1000i32) {
        prop_assert!(x * x >= 0);
    }
}

Häufige Fehler und Best Practices

Hier sind einige Tipps, um das Beste aus Proptest herauszuholen:

  • Beginnen Sie mit einfachen Strategien: Nutzen Sie any::() und Bereiche, bevor Sie komplexe prop_compose!-Strategien erstellen.
  • Testen Sie Grenzfälle explizit: Kombinieren Sie Property-basierte Tests mit traditionellen Unit-Tests für bekannte Grenzfälle.
  • Nutzen Sie prop_filter sparsam: Zu viele gefilterte Werte können die Testabdeckung verringern. Erwägen Sie stattdessen eine spezifischere Strategie.
  • Dokumentieren Sie Ihre Eigenschaften: Ein guter Eigenschaftsname erklärt, was getestet wird, und dient als lebende Dokumentation.
  • Verwenden Sie proptest-derive für benutzerdefinierte Typen: Das Ableiten von Arbitrary mit #[derive(Arbitrary)] spart viel Boilerplate-Code.

Fazit

Proptest ist ein unverzichtbares Werkzeug für jeden Rust-Entwickler, der zuverlässigen und robusten Code schreiben möchte. Es hilft, Fehler zu finden, die mit herkömmlichen Tests oft übersehen werden, und reduziert gleichzeitig den Aufwand für die Testpflege. Mit der automatischen Minimierung und den flexiblen Konfigurationsmöglichkeiten ist Proptest ideal für Projekte jeder Größe.

Beginnen Sie noch heute mit Proptest und machen Sie Ihre Rust-Tests smarter!

Empfehlungen