Testen in Rust
In Rust sind Tests direkt in die Sprache und in cargo eingebaut. Du brauchst also keine extra Bibliothek, um loszulegen. Auf dieser Seite lernst du genau so viel, dass du deine ersten Tests schreiben und ausführen kannst.
Einen einfachen Test schreiben
Abschnitt betitelt „Einen einfachen Test schreiben“Ein Test ist eine ganz normale Funktion, die mit #[test] markiert ist:
fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }}Ein paar Dinge sind hier wichtig:
#[cfg(test)]sagt Rust, dass dieses Modul nur beim Ausführen der Tests kompiliert wird. Es gehört nicht zu deinem normalen Programm.use super::*;holt alles aus dem übergeordneten Modul herein (hier die Funktionadd). So kann der Test sie aufrufen.#[test]markiert eine bestimmte Funktion als Test, dencargo testausführen soll.
Tests ausführen
Abschnitt betitelt „Tests ausführen“Im Ordner deines Projekts:
cargo testDu siehst dann etwa diese Ausgabe:
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredassert! und seine Verwandten
Abschnitt betitelt „assert! und seine Verwandten“Mit drei Makros deckst du das meiste ab:
#[test]fn checks() { assert!(2 + 2 == 4); // Test schlägt fehl, wenn die Bedingung false ist assert_eq!(2 + 2, 4); // schlägt fehl, wenn die beiden Werte nicht gleich sind assert_ne!(2 + 2, 5); // schlägt fehl, wenn die beiden Werte gleich SIND}Meistens nimmt man lieber assert_eq! und assert_ne! statt eines einfachen assert!(a == b). Der Grund: Wenn sie fehlschlagen, geben sie beide Werte aus. Du siehst also sofort, was nicht gepasst hat – und nicht nur „assertion failed“.
Ein fehlschlagender Test und wie er aussieht
Abschnitt betitelt „Ein fehlschlagender Test und wie er aussieht“fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn this_will_fail() { assert_eq!(add(2, 2), 5); }}test tests::this_will_fail ... FAILED
assertion `left == right` failed left: 4 right: 5Genau so eine Meldung willst du sehen, wenn ein Test fehlschlägt. left ist der Wert, den dein Code tatsächlich geliefert hat, right der Wert, den der Test erwartet hat.
Testen, ob eine Funktion eine Panic auslöst
Abschnitt betitelt „Testen, ob eine Funktion eine Panic auslöst“Manchmal ist eine Panic (Programmabbruch) genau das richtige Verhalten – zum Beispiel bei einer Funktion, die ungültige Eingaben ablehnen soll. Dafür gibt es #[should_panic]:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("Division durch null ist nicht möglich"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}Bessere Fehlermeldungen
Abschnitt betitelt „Bessere Fehlermeldungen“Wenn ein Test fehlschlägt, willst du so schnell wie möglich wissen, warum. Zwei kleine Ergänzungen helfen dabei sehr.
Eine eigene Meldung. assert!, assert_eq! und assert_ne! nehmen nach den Werten noch eine zusätzliche Meldung an, die du genauso schreibst wie bei println!. Sie wird nur ausgegeben, wenn die Prüfung fehlschlägt.
expected bei should_panic. Ein einfaches #[should_panic] besteht bei jeder Panic – auch bei einer, die ein ganz anderer Bug verursacht hat. Mit expected = "..." besteht der Test nur, wenn die Panic-Meldung diesen Text enthält.
Hier ist noch einmal die Funktion divide, diesmal mit einem main, damit du sie ausführen kannst, und zwei Tests, die beide Ideen benutzen:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("Division durch null ist nicht möglich"); } a / b}
fn main() { println!("10 / 2 = {}", divide(10, 2)); println!("7 / 2 = {}", divide(7, 2));}
#[cfg(test)]mod tests { use super::*;
#[test] fn rounds_down() { let result = divide(7, 2); assert_eq!(result, 3, "7 / 2 sollte abgerundet werden, ergab aber {result}"); }
#[test] #[should_panic(expected = "durch null")] fn dividing_by_zero_panics() { divide(10, 0); }}Ausgabe:
10 / 2 = 57 / 2 = 3Die Ausgabe oben ist das, was cargo run ausgibt, denn main führt die Tests nicht aus. Die Tests startest du mit cargo test. Im Rust Playground öffnest du das Menü neben Run und wählst Test. Dann siehst du:
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outTests laufen parallel, deshalb können die beiden test ...-Zeilen in beliebiger Reihenfolge erscheinen. Stell dir jetzt vor, der Test würde 4 statt 3 erwarten. Dann stünde deine Meldung in der Fehlerausgabe, direkt neben den beiden Werten:
assertion `left == right` failed: 7 / 2 sollte abgerundet werden, ergab aber 3 left: 3 right: 4Und wenn divide mit einer anderen Meldung eine Panic auslöst, fängt die expected-Prüfung das ab. Hier erwartet der Test den Text "negativ", aber die Panic-Meldung enthält ihn nicht:
note: panic did not contain expected string panic message: "Division durch null ist nicht möglich" expected substring: "negativ"Warum überhaupt Tests schreiben?
Abschnitt betitelt „Warum überhaupt Tests schreiben?“Ehrlich gesagt: Für ein kurzes Skript wirst du wahrscheinlich keine schreiben. Tests lohnen sich, sobald Code wiederverwendet oder später geändert wird. Dann kannst du ihn mit gutem Gefühl umbauen: cargo test sagt dir sofort, wenn du etwas kaputt gemacht hast – und nicht erst ein Nutzer.
Tests können auch in deiner Dokumentation stehen: Codebeispiele in ///-Doc-Kommentaren laufen ebenfalls mit cargo test. So können deine Beispiele nie veralten. Wie das geht, zeigt die Seite Dokumentation.
Übungen
Abschnitt betitelt „Übungen“Bei beiden Übungen hat die Lösung ein main, das du ausführen kannst, und Tests, die du mit cargo test startest (oder mit Test im Playground).
1. Eine Notenfunktion testen
Abschnitt betitelt „1. Eine Notenfunktion testen“Schreib fn grade(score: u32) -> char, die für 90 und mehr Punkte 'A' zurückgibt, für 80 bis 89 'B', für 70 bis 79 'C' und für alles darunter 'F'. Schreib dann Tests, die die Grenzen prüfen, zum Beispiel 90 und 89. Tipp: An den Grenzen verstecken sich die Bugs. Ein Test für genau 90 findet den klassischen Fehler, > statt >= zu schreiben.
Lösung anzeigen
fn grade(score: u32) -> char { if score >= 90 { 'A' } else if score >= 80 { 'B' } else if score >= 70 { 'C' } else { 'F' }}
fn main() { for score in [95, 85, 72, 40] { println!("{score} -> {}", grade(score)); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn edges_of_a() { assert_eq!(grade(90), 'A'); assert_eq!(grade(89), 'B'); }
#[test] fn edges_of_c() { assert_eq!(grade(70), 'C'); assert_eq!(grade(69), 'F'); }
#[test] fn zero_is_f() { assert_eq!(grade(0), 'F', "mit 0 Punkten sollte man durchfallen"); }}Ausgabe:
95 -> A85 -> B72 -> C40 -> Fcargo test sollte 3 passed melden. Ändere einmal score >= 90 zu score > 90 und führ die Tests noch einmal aus: edges_of_a schlägt fehl und zeigt dir left: 'B' und right: 'A'.
2. Eine Funktion testen, die ein Option zurückgibt
Abschnitt betitelt „2. Eine Funktion testen, die ein Option zurückgibt“Schreib fn safe_divide(a: i32, b: i32) -> Option<i32>, die None zurückgibt, wenn b gleich 0 ist, statt eine Panic auszulösen. Schreib einen Test für eine normale Division und einen für die Division durch null. Tipp: assert_eq! funktioniert auch mit Option, du kannst also assert_eq!(safe_divide(1, 0), None) schreiben.
Lösung anzeigen
fn safe_divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None } else { Some(a / b) }}
fn main() { println!("{:?}", safe_divide(10, 2)); println!("{:?}", safe_divide(1, 0));}
#[cfg(test)]mod tests { use super::*;
#[test] fn divides_normally() { assert_eq!(safe_divide(10, 2), Some(5)); }
#[test] fn zero_gives_none() { assert_eq!(safe_divide(1, 0), None); }}Ausgabe:
Some(5)NoneVergleich das mit der Funktion divide von oben. Dort ist die Division durch null eine Panic, deshalb braucht der Test #[should_panic]. Hier ist es einfach ein normaler Rückgabewert, also reicht ein schlichtes assert_eq!. Funktionen, die Option oder Result zurückgeben, lassen sich meist leichter testen.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Markiere eine Funktion mit
#[test], damit sie ein Test wird. Alle Tests führst du mitcargo testaus. assert!,assert_eq!undassert_ne!decken die meisten alltäglichen Prüfungen ab. Nimm lieberassert_eq!/assert_ne!, weil ihre Fehlermeldungen hilfreicher sind.#[should_panic]prüft, ob eine Funktion eine Panic auslöst, wenn sie das soll. Mitexpected = "..."prüfst du, ob sie aus dem richtigen Grund ausgelöst wurde.- Gib jedem Assert-Makro eine eigene Meldung mit, die erklärt, was schiefgelaufen ist, wenn die Prüfung fehlschlägt.
- Am meisten bringen Tests bei Code, den du später ändern oder wiederverwenden willst.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Welches Attribut markiert eine Funktion als Test, den cargo test ausführen soll?
2. Warum nimmt man meistens assert_eq! und assert_ne! statt eines einfachen assert!(a == b)?
3. Was macht #[should_panic]?
Punkte: 0 / 3
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Tests schreiben – Das Rust-Buch:
#[test], die Assert-Makros, eigene Meldungen,should_panicund Tests, die einResultzurückgeben. - Steuern, wie Tests ausgeführt werden – Das Rust-Buch: einen einzelnen Test über seinen Namen starten, die Ausgabe von
println!anzeigen und#[ignore]. - Testverwaltung – Das Rust-Buch: der Unterschied zwischen Unit-Tests und Integrationstests im Ordner
tests/. - Unit testing – Rust by Example (auf Englisch): kurze Beispiele für bestandene, fehlgeschlagene und abstürzende Tests.
cargo test– The Cargo Book (auf Englisch): alle Optionen des Befehlscargo test.