Testen in Rust: Von Unit-Tests bis Property-based Testing
Rust ist eine der wenigen Sprachen, bei denen das Typ- und Besitzsystem bereits zur Kompilierzeit ganze Fehlerklassen ausschließt (Datenrennen, hängende Zeiger, doppelte Speicherfreigabe). Logische Fehler, fehlerhafte Behandlung von Randfällen und falsche Geschäftslogik bleiben jedoch bestehen. Um diese zu erkennen, hat sich im Rust-Ökosystem ein leistungsstarker Satz von Testwerkzeugen etabliert – vom integrierten Test-Runner bis hin zu fortgeschrittenen Frameworks zur Generierung von Testdaten.
Integrierte Werkzeuge: cargo test
Das grundlegende Testwerkzeug ist direkt in den Compiler und Paketmanager integriert. Um Tests zu erstellen, genügt es, ein Modul mit dem Attribut #[cfg(test)] hinzuzufügen und die Funktionen mit dem Attribut #[test] zu kennzeichnen.
// src/lib.rspub fn add(left: u64, right: u64) -> u64 { left + right}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_add() { let result = add(2, 2); assert_eq!(result, 4); }
#[test] fn test_add_negative() { // u64 unterstützt keine negativen Zahlen – das ist ein Kompilierfehler // Aber wenn die Funktion i64 akzeptieren würde: // assert_eq!(add(-2, 2), 0); }}Aufruf: cargo test. Das Flag -- --nocapture gibt println! aus Tests aus. Das Flag -- --test-threads=1 deaktiviert die parallele Ausführung.
Integrationstests
Integrationstests befinden sich im Ordner tests/ auf der Stammebene des Projekts. Jede Datei ist eine separate Crate, die die öffentliche API der Bibliothek testen kann.
// tests/integration_test.rsuse my_library::add;
#[test]fn test_integration() { assert_eq!(add(10, 20), 30);}Zum Testen von Binär-Crates (mit main.rs) kann assert_cmd verwendet werden – es startet die Binärdatei als externen Prozess und prüft stdout/stderr.
Property-based Testing: proptest und quickcheck
Anstatt Dutzende von Testfällen manuell zu schreiben, generiert Property-based Testing Tausende von zufälligen Eingabedaten und prüft Invarianten. Die beiden wichtigsten Frameworks:
- proptest – modern, flexibel, mit Unterstützung für Strategien, Filterung und Beispielkomprimierung (Shrink).
- quickcheck – älter, inspiriert von Haskell QuickCheck, einfacher, aber weniger leistungsstark.
Beispiel mit proptest:
use proptest::prelude::*;
fn reverse_string(s: &str) -> String { s.chars().rev().collect()}
proptest! { #[test] fn test_reverse_twice(s in ".*") { let reversed = reverse_string(&s); let double_reversed = reverse_string(&reversed); assert_eq!(s, double_reversed); }
#[test] fn test_reverse_length(s in ".*") { let reversed = reverse_string(&s); assert_eq!(s.len(), reversed.len()); }}Wenn ein Test fehlschlägt, findet proptest ein minimales Gegenbeispiel (Shrinking).
Testen von asynchronem Code: tokio-test
Für asynchrone Funktionen, die mit tokio arbeiten, sind normale Tests nicht geeignet – es wird eine asynchrone Laufzeitumgebung benötigt. tokio-test stellt das Makro #[tokio::test] bereit.
use tokio::time::{sleep, Duration};
async fn delayed_add(a: u32, b: u32) -> u32 { sleep(Duration::from_millis(10)).await; a + b}
#[tokio::test]async fn test_delayed_add() { let result = delayed_add(3, 5).await; assert_eq!(result, 8);}Es kann auch futures-test für die Arbeit mit futures und async-std verwendet werden.
Mocks und Stubs: mockall und mockito
Zur Isolierung von Tests von externen Abhängigkeiten (DB, API, Dateisystem) werden Mocking-Bibliotheken verwendet. mockall generiert Mock-Objekte für Traits, mockito simuliert HTTP-Server.
// Beispiel mit mockalluse mockall::automock;
#[automock]pub trait Database { fn get_user(&self, id: u32) -> Option;}
#[tokio::test]async fn test_get_user() { let mut mock = MockDatabase::new(); mock.expect_get_user() .with(predicate::eq(42)) .returning(|_| Some("Alice".to_string())); let result = mock.get_user(42); assert_eq!(result, Some("Alice".to_string()));} Für HTTP-Mocks wird mockito verwendet:
use mockito::Server;
#[tokio::test]async fn test_api_call() { let mut server = Server::new(); let mock = server.mock(\"GET\", \"/api/data\") .with_status(200) .with_body(\"{\\\"key\\\": \\\"value\\\"}\") .create(); // Code, der die API aufruft... mock.assert();}Benchmarking: criterion
Für Leistungstests wird criterion verwendet – ein statistisch fundiertes Benchmarking-Framework. Es misst die Ausführungszeit, erstellt Diagramme und erkennt Regressionen.
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 { match n { 0 => 0, 1 => 1, _ => fibonacci(n - 1) + fibonacci(n - 2), }}
fn criterion_benchmark(c: &mut Criterion) { c.bench_function(\"fibonacci 20\", |b| b.iter(|| fibonacci(black_box(20))));}
criterion_group!(benches, criterion_benchmark);criterion_main!(benches);Testabdeckung: tarpaulin und grcov
Zur Messung der Codeabdeckung in Rust werden tarpaulin (für Linux) und grcov (plattformübergreifend) verwendet. cargo tarpaulin erstellt einen HTML-Bericht mit detaillierter Abdeckung.
Praktische Tipps
- Testen Sie öffentliche APIs – private Funktionen werden indirekt getestet.
- Verwenden Sie
#[should_panic]für Tests, die einen Panic erwarten. - Nutzen Sie
assert_eq!/assert_ne!anstelle vonassert!(a == b)– die Fehlermeldungen sind informativer. - Dokumentationstests (
/// ```) testen Codebeispiele in der Dokumentation. - Führen Sie
cargo testvor jedem Commit aus – idealerweise in CI.
Fazit
Das Rust-Ökosystem bietet alles für qualitativ hochwertiges Testen: von einfachen Unit-Tests über Property-based Testing bis hin zu asynchronen Tests und Mocks. Die Integration in cargo macht den Einstieg einfach, und leistungsstarke Bibliotheken wie proptest und criterion helfen, auch komplexe Szenarien abzudecken. Die Kombination aus Typsicherheit und einem durchdachten Testansatz macht Rust zu einer der zuverlässigsten Sprachen für die Entwicklung kritischer Systeme.
Auch in der Bibliothek
ts-morph für TypeScript: Komplettguide mit Beispielen
Arbeiten mit Kafka in TypeScript: Die Bibliothek KafkaJS für Node.js
Maschinelles Lernen mit Rust: Überblick über Bibliotheken für ML und Data Science
mimalloc für C++: Vollständige Anleitung mit Beispielen