Tests schreiben
Tests sind Funktionen in Rust, die überprüfen, ob der zu testende Code in der erwarteten Weise funktioniert. Der Rumpf von Testfunktionen führt in der Regel diese drei Aktionen aus:
- Bereite die benötigten Daten und Zustände vor.
- Führe den Code aus, den du testen möchtest.
- Stelle sicher, dass die Ergebnisse die sind, was du erwartest.
Schauen wir uns die Funktionalität an, die Rust speziell für das Schreiben von
Tests bereitstellt, die diese Aktionen ausführen. Dazu gehören das Attribut
test
, einige Makros und das Attribut should_panic
.
Anatomie einer Testfunktion
Im einfachsten Fall ist ein Test in Rust eine Funktion, die mit dem Attribut
test
annotiert wird. Attribute sind Metadaten über Teile des Rust-Codes; ein
Beispiel ist das Attribut derive
, das wir in Kapitel 5 bei Strukturen
verwendet haben. Um eine Funktion in eine Testfunktion zu verwandeln, füge
#[test]
oberhalb der Zeile mit fn
ein. Wenn du deine Tests mit dem Befehl
cargo test
ausführst, erstellt Rust eine Testausführungs-Binärdatei (test
runner binary), die die annotierte Funktionen ausführt und darüber berichtet,
ob jede Testfunktion erfolgreich war oder nicht.
Wann immer wir ein neues Bibliotheksprojekt mit Cargo durchführen, wird für uns automatisch ein Testmodul mit einer Testfunktion darin generiert. Dieses Modul gibt dir eine Vorlage, um deine Tests zu schreiben, sodass du nicht jedes Mal, wenn du ein neues Projekt startest, die genaue Struktur und Syntax nachschlagen musst. Du kannst so viele zusätzliche Testfunktionen und Testmodule hinzufügen, wie du möchtest!
Wir werden einige Aspekte der Funktionsweise von Tests untersuchen, indem wir mit der Testvorlage experimentieren, bevor wir tatsächlich Code testen. Dann schreiben wir einige Tests aus der realen Welt, die einen von uns geschriebenen Code aufrufen und sicherstellen, dass sein Verhalten korrekt ist.
Lass uns ein neues Bibliotheksprojekt namens adder
erstellen, das zwei Zahlen
addiert:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
Der Inhalt der Datei src/lib.rs in deiner Bibliothek adder
sollte wie
Codeblock 11-1 aussehen.
Dateiname: src/lib.rs
#![allow(unused)] fn main() { pub fn add(left: usize, right: usize) -> usize { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); } } }
Konzentrieren wir uns zunächst nur auf die Funktion it_works
. Beachte die
Annotation #[test]
: Dieses Attribut zeigt an, dass es sich um eine
Testfunktion handelt, sodass die Testausführung weiß, dass es diese Funktion
als einen Test behandeln soll. Wir könnten auch Nicht-Test-Funktionen im Modul
tests
haben, um gängige Szenarien aufzusetzen oder gängige Operationen
durchzuführen, daher müssen wir immer angeben, welche Funktionen Tests sind.
Der Beispiel-Funktionsrumpf verwendet das Makro assert_eq!
, um
sicherzustellen, dass 2 + 2 gleich 4 ist. Diese Prüfung dient als Beispiel für
den Aufbau eines typischen Tests. Lassen wir ihn laufen, um zu sehen, dass
dieser Test erfolgreich ist.
Das Kommando cargo test
führt alle Tests in unserem Projekt aus, wie in
Codeblock 11-2 zu sehen ist.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Cargo hat den Test kompiliert und ausgeführt. Nach den Zeilen Compiling
,
Finished
und Running
folgt die Zeile running 1 test
. Die nächste Zeile
zeigt den Namen der generierten Testfunktion tests::it_works
und das Ergebnis
der Testausführung: ok
. Als nächstes wird die Gesamtzusammenfassung der
Testausführung angegeben. Der Text test result: ok.
bedeutet, dass alle Tests
bestanden wurden, und der Teil 1 passed; 0 failed
gibt die Anzahl der Tests
an, die bestanden oder nicht bestanden wurden.
Es ist möglich, einen Test als ignoriert zu markieren, sodass er in einer
bestimmten Instanz nicht ausgeführt wird; wir werden dies im Abschnitt „Tests
ignorieren, die nicht ausdrücklich verlangt werden“ später in
diesem Kapitel behandeln. Da wir das hier nicht getan haben, zeigt die
Zusammenfassung 0 ignored
.
Die Statistik 0 measured
ist für Benchmark-Tests, die die Performanz messen.
Benchmark-Tests sind zum Zeitpunkt, als dieser Text verfasst wurde, nur im
nächtlichen (nightly) Rust verfügbar. Siehe „Dokumentation über
Benchmark-Tests“, um mehr zu erfahren.
Wir können auch ein Argument an den Befehl cargo test
übergeben, um nur
Tests auszuführen, deren Name mit einer Zeichenkette übereinstimmt; dies wird
Filtern genannt und wir werden dies im Abschnitt „Ausführen einer
Test-Teilmenge mittels Name“ behandeln. Außerdem haben wir die
durchgeführten Tests nicht gefiltert, sodass am Ende der Zusammenfassung 0 filtered out
steht.
Der nächste Teil der Testausgabe, der mit Doc-tests adder
beginnt, ist für
die Ergebnisse von Dokumentationstests. Wir haben noch keine
Dokumentationstests, aber Rust kann alle Code-Beispiele kompilieren, die in
unserer API-Dokumentation erscheinen. Diese Funktionalität hilft dabei,
deine Dokumentation und deinen Code synchron zu halten! Wie man
Dokumentationstests schreibt, werden wir im Abschnitt
„Dokumentationskommentare als Tests“ in Kapitel 14 besprechen.
Vorerst ignorieren wir die Ausgabe von Doc-tests
.
Beginnen wir damit, den Test an unsere eigenen Bedürfnisse anzupassen. Ändere
zunächst den Namen der Funktion it_works
in einen anderen Namen, z.B.
exploration
, wie folgt:
Dateiname: src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Dann führe cargo test
erneut aus. Die Ausgabe zeigt nun exploration
anstelle von it_works
:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Fügen wir einen weiteren Test hinzu, aber dieses Mal machen wir einen Test, der
fehlschlägt! Tests schlagen fehl, wenn etwas die Testfunktion zum Abbrechen
bringt. Jeder Test wird in einem neuen Strang (thread) ausgeführt und wenn der
Hauptstrang (main thread) sieht, dass ein Teststrang (test thread) abgebrochen
wurde, wird der Test als fehlgeschlagen markiert. Über den einfachsten Weg, ein
Programm abzubrechen, sprachen wir in Kapitel 9, und zwar durch den Aufruf des
Makros panic!
. Erstelle einen neuen Test another
, sodass deine Datei
src/lib.rs wie in Codeblock 11-3 aussieht.
Dateiname: src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Lasse diesen Test fehlschlagen");
}
}
Führe die Tests erneut mit cargo test
aus. Die Ausgabe sollte wie in
Codeblock 11-4 aussehen, was zeigt, dass unser Test exploration
bestanden und
another
fehlgeschlagen ist.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at src/lib.rs:10:9:
Lasse diesen Test fehlschlagen
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Statt ok
zeigt die Zeile test tests::another
das Ergebnis FAILED
.
Zwischen den Einzelergebnissen und der Zusammenfassung erscheinen zwei neue
Abschnitte: Der erste zeigt die detaillierte Ursache für jeden fehlgeschlagenen
Test an. In diesem Fall erhalten wir Details, dass another
scheiterte mit der
Meldung panicked at 'Lasse diesen Test fehlschlagen'
in Zeile 17 der Datei
src/lib.rs. Der nächste Abschnitt listet nur die Namen aller fehlgeschlagenen
Tests auf, was nützlich ist, wenn es viele Tests und viele detaillierte
Ausgaben von fehlgeschlagenen Tests gibt. Wir können den Namen eines
fehlgeschlagenen Tests verwenden, um genau diesen Test auszuführen, um ihn
leichter zu debuggen; wir werden im Abschnitt „Steuern wie Tests ausgeführt
werden“ mehr über Möglichkeiten zur Ausführung
von Tests sprechen.
Die Zusammenfassungszeile zeigt am Ende an: Insgesamt ist unser Testergebnis
FAILED
. Wir hatten einen Test bestanden und einen Test nicht bestanden.
Da du nun gesehen hast, wie die Testergebnisse in verschiedenen Szenarien
aussehen, wollen wir uns einige Makros neben panic!
ansehen, die bei Tests
nützlich sind.
Ergebnisse überprüfen mit dem Makro assert!
Das Makro assert!
, das von der Standardbibliothek bereitgestellt wird, ist
nützlich, wenn du sicherstellen willst, dass eine Bedingung in einem Test als
wahr (true) bewertet wird. Wir geben dem Makro assert!
ein Argument, das
boolesch ausgewertet wird. Wenn der Wert true
ist, passiert nichts und der
Test ist bestanden. Wenn der Wert false
ist, ruft das Makro assert!
das
Makro panic!
auf, um den Test fehlschlagen zu lassen. Das Verwenden des
Makros assert!
hilft uns zu überprüfen, ob unser Code so funktioniert, wie
wir es beabsichtigen.
In Codeblock 5-15 in Kapitel 5 haben wir eine Struktur Rectangle
und eine
Methode can_hold
verwendet, die hier in Codeblock 11-5 wiederholt werden.
Lass uns diesen Code in die Datei src/lib.rs packen und dann einige Tests
dafür mit dem Makro assert!
schreiben.
Dateiname: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Die Methode can_hold
gibt ein Boolean zurück, was bedeutet, dass es ein
perfekter Anwendungsfall für das Makro assert!
ist. In Codeblock 11-6
schreiben wir einen Test, der die Methode can_hold
überprüft, indem wir eine
Rectangle
-Instanz mit einer Breite von 8 und einer Höhe von 7 erstellen und
sicherstellen, dass es eine weitere Rectangle
-Instanz mit einer Breite von 5
und einer Höhe von 1 enthalten kann.
Dateiname: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
Beachte die Zeile use super::*;
im Modul tests
. Das Modul tests
ist ein
reguläres Modul, das den üblichen Sichtbarkeitsregeln folgt, die wir in Kapitel
7 im Abschnitt „Mit Pfaden auf ein Element im Modulbaum
verweisen“ behandelt haben.
Da das Modul tests
ein inneres Modul ist, müssen wir den Code, der im äußeren
Modul getestet wird, in den Gültigkeitsbereich des inneren Moduls bringen. Wir
verwenden hier einen Stern (glob), sodass alles, was wir im äußeren Modul
definieren, auch in diesem Modul tests
zur Verfügung steht.
Wir haben unseren Test larger_can_hold_smaller
genannt und wir haben die
beiden Rectangle
-Instanzen erzeugt, die wir benötigen. Dann haben wir das
Makro assert!
aufgerufen und ihm das Aufrufergebnis von
larger.can_hold(&smaller)
übergeben. Dieser Ausdruck soll true
zurückgeben,
also sollte unser Test erfolgreich sein. Lass es uns herausfinden!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Es funktioniert! Fügen wir noch einen weiteren Test hinzu, diesmal mit der Zusicherung, dass ein kleineres Rechteck nicht in ein größeres Rechteck passt:
Dateiname: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// --abschneiden--
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
Da das korrekte Ergebnis der Funktion can_hold
in diesem Fall false
ist,
müssen wir dieses Ergebnis negieren, bevor wir es an das Makro assert!
übergeben. Als Ergebnis wird unser Test bestehen, wenn can_hold
den
Rückgabewert false
hat:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Zwei Tests, die erfolgreich sind! Nun wollen wir sehen, was mit unseren
Testergebnissen passiert, wenn wir einen Fehler in unseren Code einbringen.
Wir ändern die Implementierung der Methode can_hold
, indem wir das
größer-als-Zeichen durch ein kleiner-als-Zeichen ersetzen, wenn sie die Breiten
vergleicht:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// --abschneiden--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
Das Ausführen der Tests ergibt nun Folgendes:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Unsere Tests haben den Fehler entdeckt! Da larger.width
gleich 8
ist und
smaller.width
gleich 5
, ergibt der Vergleich der Breiten in can_hold
jetzt
false
: 8 ist nicht kleiner als 5.
Prüfung auf Gleichheit mit den Makros assert_eq!
und assert_ne!
Eine übliche Methode zum Verifizieren von Funktionalität besteht darin, das
Ergebnis des zu testenden Codes auf Gleichheit mit dem Wert zu testen, den du
vom Code erwartest, um sicherzustellen. Du könntest dies mit dem Makro
assert!
tun und ihm einen Ausdruck mit dem Operator ==
übergeben. Dies ist
jedoch ein so häufiger Testfall, dass die Standardbibliothek zwei Makros zur
Verfügung stellt, um diesen Test bequemer durchzuführen: assert_eq!
und
assert_ne!
. Diese Makros vergleichen zwei Argumente auf Gleichheit bzw.
Ungleichheit. Sie geben auch die beiden Werte aus, wenn die Zusicherung
fehlschlägt, was es einfacher macht zu erkennen, warum der Test
fehlgeschlagen ist; umgekehrt zeigt das Makro assert!
nur an, dass der
Ausdruck ==
den Wert false
ergeben hat, ohne die Werte auszugeben, die zum
falschen Testergebnis geführt haben.
In Codeblock 11-7 schreiben wir eine Funktion namens add_two
, die zu ihrem
Parameter 2
addiert, dann testen wir diese Funktion mit dem Makro
assert_eq!
.
Dateiname: src/lib.rs
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
Lass uns prüfen, ob sie den Test besteht!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Wir erstellen eine Variable namens result
, die das Ergebnis des Aufrufs von
add_two(2)
enthält. Dann übergeben wir result
und 4
als Argumente an
assert_eq!
. Die Ausgabezeile für diesen Test lautet test tests::it_adds_two ... ok
, und das Wort ok
gibt an, dass unser Test bestanden wurde!
Lass uns einen Fehler in unseren Code einbringen, um zu sehen, wie assert_eq!
aussieht, wenn es fehlschlägt. Ändern wir die Implementierung der Funktion
add_two
, sodass sie stattdessen 3
addiert:
pub fn add_two(a: i32) -> i32 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
Führe die Tests erneut aus:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at src/lib.rs:11:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Unser Test hat den Fehler entdeckt! Der Test it_adds_two
schlug fehl mit der
Meldung assertion `left == right` failed
und den Werten für left
und
right
. Diese Nachricht hilft uns, mit der Fehlersuche zu beginnen: Das
Argument left
mit dem Ergebnis von add_two(2)
war 5
, aber das Argument
right
war 4
. Du kannst dir vorstellen, dass dies besonders hilfreich ist,
wenn wir viele Tests durchführen.
Beachte, dass in einigen Sprachen und Test-Bibliotheken die Parameter der
Gleichheitszusicherung expected
und actual
genannt werden und deren
Reihenfolge wichtig ist. In Rust werden sie jedoch left
und right
genannt
und die Reihenfolge, in der wir den erwarteten Wert und den vom Code
produzierten Wert angeben, spielt keine Rolle. Wir könnten die Zusicherung in
diesem Test als assert_eq!(4, result)
schreiben, was zur selben
Fehlermeldung assertion failed: `(left == right)`
führen würde.
Das Makro assert_ne!
prüft, ob die beiden Werte, die wir ihm übergeben,
ungleich sind und scheitert, wenn sie gleich sind. Dieses Makro ist am
nützlichsten in Fällen, in denen wir nicht sicher sind, welchen Wert wir
bekommen werden, aber wir wissen, welcher Wert es definitiv nicht sein sollte.
Wenn wir zum Beispiel eine Funktion testen, die ihre Eingabe garantiert in
irgendeiner Weise verändert, aber die Art und Weise, wie die Eingabe verändert
wird, vom Wochentag abhängt, an dem wir unsere Tests ausführen, ist es
vielleicht am besten sicherzustellen, dass die Ausgabe der Funktion nicht
gleich der Eingabe ist.
Unter der Haube verwenden die Makros assert_eq!
und assert_ne!
die
Operatoren ==
bzw. !=
. Wenn die Zusicherungen fehlschlagen, geben diese
Makros ihre Argumente unter Verwendung der Debug-Formatierung aus, was
bedeutet, dass die zu vergleichenden Werte die Merkmale PartialEq
und
Debug
implementieren müssen. Alle primitiven Typen und die meisten
Standardbibliothekstypen implementieren diese Merkmale. Für Strukturen und
Aufzählungen, die du definierst, musst du PartialEq
implementieren, um
die Gleichheit dieser Typen sicherzustellen. Du musst auch Debug
implementieren, um die Werte auszugeben, wenn die Zusicherung fehlschlägt. Da
es sich bei beiden Merkmalen um ableitbare Merkmale handelt, wie in Codeblock
5-12 in Kapitel 5 erwähnt, genügt normalerweise das Ergänzen der Annotation
#[derive(PartialEq, Debug)]
bei deiner Struktur- und Aufzählungsdefinition.
Siehe Anhang C „Ableitbare Merkmale (traits)“ für weitere
Einzelheiten über diese und andere ableitbare Merkmale.
Benutzerdefinierte Fehlermeldungen angeben
Du kannst den Makros assert!
, assert_eq!
und assert_ne!
optional auch
eine benutzerdefinierte Nachricht mitgeben, die mit der Fehlermeldungen
ausgegeben wird. Alle Argumente, die nach den erforderlichen Argumenten
angegeben werden, werden an das Makro format!
übergeben (siehe Kapitel 8,
Abschnitt „Aneinanderhängen mit dem Operator +
und dem Makro
format!
“), sodass du
eine Formatierungs-Zeichenkette übergeben kannst, die Platzhalter {}
und
Werte enthält, die in diese Platzhalter gehören. Benutzerdefinierte Nachrichten
sind nützlich, um zu dokumentieren, was eine Zusicherung bedeutet; wenn ein
Test fehlschlägt, hast du eine bessere Vorstellung davon, wo das Problem im
Code liegt.
Nehmen wir zum Beispiel an, wir haben eine Funktion, die Leute mit Namen begrüßt, und wir wollen testen, ob der Name, den wir an die Funktion übergeben, in der Ausgabe auftaucht:
Dateiname: src/lib.rs
pub fn greeting(name: &str) -> String {
format!("Hallo {name}!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
Die Anforderungen für dieses Programm sind noch nicht abgestimmt worden und wir
sind ziemlich sicher, dass sich der Text Hallo
zu Beginn der Begrüßung ändern
wird. Wir haben beschlossen, dass wir den Test nicht aktualisieren wollen, wenn
sich die Anforderungen ändern. Anstatt also zu prüfen, ob der Test exakt dem
von der Funktion greeting
zurückgegebenen Wert entspricht, stellen wir
einfach sicher, dass die Ausgabe den Text des Eingabeparameters enthält.
Lass uns nun einen Fehler in diesen Code einbringen, indem wir greeting
so
ändern, dass name
nicht enthalten ist, um zu sehen, wie das
Standard-Testversagen aussieht:
pub fn greeting(name: &str) -> String {
String::from("Hallo!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
Das Ausführen dieses Tests führt zu folgender Ausgabe:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Dieses Ergebnis zeigt nur an, dass die Zusicherung fehlgeschlagen ist und in
welcher Zeile die Zusicherung steht. Eine nützlichere Fehlermeldung würde den
Wert der Funktion greeting
ausgeben. Fügen wir eine benutzerdefinierte
Fehlermeldung hinzu, die aus einer Formatierungszeichenkette mit einem
Platzhalter besteht, der mit dem tatsächlichen Wert aus der Funktion greeting
gefüllt ist:
#![allow(unused)] fn main() { pub fn greeting(name: &str) -> String { String::from("Hallo!") } #[cfg(test)] mod tests { use super::*; #[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!( result.contains("Carol"), "Begrüßung enthielt nicht den Namen, Wert war `{result}`" ); } } }
Wenn wir jetzt den Test ausführen, erhalten wir eine aussagekräftigere Fehlermeldung:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Begrüßung enthielt nicht den Namen, Wert war `Hallo!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Wir können den Wert, den wir tatsächlich erhalten haben, in der Testausgabe sehen, was uns helfen wird, das zu debuggen, was passiert ist, anstatt das, was wir erwartet hatten.
Mit should_panic
auf Programmabbrüche prüfen
Neben der Prüfung von Rückgabewerten ist es auch wichtig zu prüfen, ob unser
Code Fehlerbedingungen so behandelt, wie wir es erwarten. Denke zum Beispiel an
den Typ Guess
, den wir in Kapitel 9 in Codeblock 9-13 erstellt haben. Anderer
Code, der Guess
verwendet, hängt von der Garantie ab, dass Guess
-Instanzen
nur Werte zwischen 1 und 100 enthalten. Wir können einen Test schreiben, der
sicherstellt, dass der Versuch, eine Guess
-Instanz mit einem Wert außerhalb
dieses Bereichs zu erzeugen, zum Programmabbrucht führt.
Wir tun dies, indem wir das Attribut should_panic
zu unserer Testfunktion
hinzufügen. Der Test gilt als bestanden, wenn der Code innerhalb der Funktion
abbricht; der Test schlägt fehl, wenn der Code innerhalb der Funktion nicht
abbricht.
Codeblock 11-8 zeigt einen Test, der prüft, ob die Fehlerbedingungen von
Guess::new
eintreten, wenn wir dies erwarten.
Dateiname: src/lib.rs
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Schätzwert muss zwischen 1 und 100 liegen, ist {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
Wir setzen das Attribut #[should_panic]
hinter das Attribut #[test]
und vor
die Testfunktion, auf die sie sich bezieht. Schauen wir uns das Ergebnis an,
wenn dieser Test bestanden ist:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Sieht gut aus! Lass uns nun einen Fehler in unseren Code einbringen, indem wir
die Bedingung entfernen, bei der die Funktion new
das Programm abbricht, wenn
der Wert größer als 100 ist:
pub struct Guess {
value: i32,
}
// --abschneiden--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Schätzwert muss zwischen 1 und 100 liegen, ist {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
Wenn wir den Test in Codeblock 11-8 ausführen, wird er fehlschlagen:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Wir erhalten in diesem Fall keine sehr hilfreiche Meldung, aber wenn wir uns
die Testfunktion ansehen, sehen wir, dass sie mit #[should_panic]
annotiert
ist. Der Fehler, den wir erhielten, bedeutet, dass der Code in der Testfunktion
keinen Programmabbruch verursacht hat.
Tests, die should_panic
verwenden, können ungenau sein. Ein Test mit
should_panic
würde auch dann bestanden werden, wenn der Test aus einem
anderen Grund zum Programmabbrucht führt als dem, den wir erwartet haben. Um
Tests mit should_panic
präziser zu machen, können wir beim
should_panic
-Attribut einen optionalen Parameter expected
ergänzen. Das
Testsystem stellt sicher, dass die Fehlermeldung den angegebenen Text enthält.
Betrachte zum Beispiel den modifizierten Code für Guess
in Codeblock 11-9, wo
die Funktion new
mit unterschiedlichen Meldungen das Programm abbricht, je
nachdem, ob der Wert zu klein oder zu groß ist.
Dateiname: src/lib.rs
pub struct Guess {
value: i32,
}
// --abschneiden--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Schätzwert muss größer oder gleich 1 sein, ist {value}.");
} else if value > 100 {
panic!("Schätzwert muss kleiner oder gleich 100 sein, ist {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "kleiner oder gleich 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Dieser Test wird bestanden werden, weil der Wert, den wir beim Parameter
expected
des should_panic
-Attributs angeben, eine Teilzeichenkette der
Nachricht ist, mit der die Funktion Guess::new
das Programm abbricht. Wir
hätten die gesamte erwartete Abbruchsnachricht angeben können, in diesem Fall
also Schätzwert muss kleiner oder gleich 100 sein, ist 200
. Was du angibst,
hängt davon ab, wie viel von der Abbruchsnachricht eindeutig oder dynamisch ist
und wie präzise dein Test sein soll. In diesem Fall reicht eine
Teilzeichenkette der Abbruchsnachricht aus, um sicherzustellen, dass der Code
in der Testfunktion den Fall else if value > 100
ausführt.
Um zu sehen, was passiert, wenn ein Test mit should_panic
und einer
expected
-Nachricht fehlschlägt, wollen wir wieder einen Fehler in unseren
Code einbringen, indem wir die Zweige if value < 1
und else if value > 100
vertauschen:
#![allow(unused)] fn main() { pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!("Schätzwert muss kleiner oder gleich 100 sein, ist {value}."); } else if value > 100 { panic!("Schätzwert muss größer oder gleich 1 sein, ist {value}."); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "kleiner oder gleich 100")] fn greater_than_100() { Guess::new(200); } } }
Wenn wir diesmal den should_panic
-Test ausführen, wird er fehlschlagen:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at src/lib.rs:13:13:
Schätzwert muss größer oder gleich 1 sein, ist 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Schätzwert muss größer oder gleich 1 sein, ist 200."`,
expected substring: `"kleiner oder gleich 100"`
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Die Fehlermeldung zeigt an, dass dieser Test tatsächlich wie erwartet das
Programm abgebrochen hat, aber die Abbruchsmeldung enthielt nicht die erwartete
Zeichenfolge "kleiner oder gleich 100"
. Die Abbruchsmeldung, die wir in
diesem Fall erhielten, lautete: Schätzwert muss größer oder gleich 1 sein, ist 200.
Jetzt können wir anfangen herauszufinden, wo unser Fehler liegt!
Verwenden von Result<T, E>
in Tests
Bei unseren bisherigen Tests sind alle abgebrochen, wenn sie fehlgeschlagen
sind. Wir können auch Tests schreiben, die Result<T, E>
verwenden! Hier ist
der Test aus Codeblock 11-1 so umgeschrieben, dass er Result<T, E>
verwendet
und Err
zurückgibt, anstatt das Programm abzubrechen:
#![allow(unused_variables)]
fn main() {}
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("zwei plus zwei ist nicht gleich vier"))
}
}
}
Die Funktion it_works
hat jetzt den Rückgabetyp Result<(), String>
. Anstatt
das Makro assert_eq!
aufzurufen, geben wir im Funktionsrumpf Ok(())
zurück,
wenn der Test bestanden ist, und ein Err
mit einem String
im Inneren, wenn
der Test fehlschlägt.
Wenn du Tests so schreibst, dass sie ein Result<T, E>
zurückgeben, kannst du
den Fragezeichen-Operator im Testrumpf verwenden, was eine bequeme Möglichkeit
sein kann, Tests zu schreiben, die fehlschlagen sollten, wenn irgendeine
Operation darin eine Err
-Variante zurückgibt.
Du kannst die Annotation #[should_panic]
nicht für Tests verwenden, die
Result<T, E>
verwenden. Um sicherzustellen, dass eine Operation eine
Err
-Variante zurückgibt, verwende nicht den Fragezeichen-Operator auf den
Result<T, E>
-Wert. Verwende stattdessen assert!(value.is_err())
.
Da du nun verschiedene Möglichkeiten kennst, Tests zu schreiben, lass uns einen
Blick darauf werfen, was passiert, wenn wir unsere Tests ausführen, und die
verschiedenen Optionen untersuchen, die wir mit cargo test
verwenden können.