Zum Inhalt springen

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.

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 Funktion add). So kann der Test sie aufrufen.
  • #[test] markiert eine bestimmte Funktion als Test, den cargo test ausführen soll.

Im Ordner deines Projekts:

Terminal window
cargo test

Du siehst dann etwa diese Ausgabe:

running 1 test
test tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored

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“.

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: 5

Genau 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.

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);
}
}

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 = 5
7 / 2 = 3
▶ Selbst ausprobieren

Die 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 tests
test tests::rounds_down ... ok
test tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Tests 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: 4

Und 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"

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.

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).

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 -> A
85 -> B
72 -> C
40 -> F
▶ Selbst ausprobieren

cargo 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)
None
▶ Selbst ausprobieren

Vergleich 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.

  • Markiere eine Funktion mit #[test], damit sie ein Test wird. Alle Tests führst du mit cargo test aus.
  • assert!, assert_eq! und assert_ne! decken die meisten alltäglichen Prüfungen ab. Nimm lieber assert_eq!/assert_ne!, weil ihre Fehlermeldungen hilfreicher sind.
  • #[should_panic] prüft, ob eine Funktion eine Panic auslöst, wenn sie das soll. Mit expected = "..." 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

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