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_filtersparsam: 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-derivefür benutzerdefinierte Typen: Das Ableiten vonArbitrarymit#[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!
Auch in der Bibliothek
Logging für JavaScript (Node.js): Die besten Lösungen und Bibliotheken
Arbeiten mit ZIP-Archiven in PHP: ZipArchive und Bibliotheken zur Komprimierung
Elasticsearch-Client für Go (Golang): Vollständiger Leitfaden
WebSocket-Bibliotheken für TypeScript: ws, Socket.IO, uWebSockets.js
Clap für Rust: Vollständige Anleitung mit Beispielen