Wie man Tests schreibt

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu How to Write Tests. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab lernen wir, wie man in Rust Tests mit Attributen, Makros und Assertionen schreibt.

Wie man Tests schreibt

Tests sind Rust-Funktionen, die überprüfen, ob der nicht-testende Code auf die erwartete Weise funktioniert. Die Körper von Testfunktionen führen normalerweise diese drei Aktionen aus:

  • Legen Sie alle erforderlichen Daten oder Zustände fest.
  • Führen Sie den Code aus, den Sie testen möchten.
  • Stellen Sie sicher, dass die Ergebnisse die erwarteten sind.

Schauen wir uns die Features von Rust an, die speziell für das Schreiben von Tests zur Ausführung dieser Aktionen zur Verfügung stehen, darunter das test-Attribut, einige Makros und das should_panic-Attribut.

Die Struktur einer Testfunktion

Im einfachsten Fall ist ein Test in Rust eine Funktion, die mit dem test-Attribut annotiert ist. Attribute sind Metadaten zu Teilen von Rust-Code; ein Beispiel ist das derive-Attribut, das wir in Kapitel 5 mit Structs verwendet haben. Um eine Funktion in eine Testfunktion umzuwandeln, fügen Sie #[test] in der Zeile vor fn hinzu. Wenn Sie Ihre Tests mit dem Befehl cargo test ausführen, baut Rust einen Testrunner-Binär aus, der die annotierten Funktionen ausführt und meldet, ob jede Testfunktion erfolgreich abgeschlossen oder fehlschlägt.

Wenn wir mit Cargo ein neues Bibliotheksprojekt erstellen, wird automatisch ein Testmodul mit einer Testfunktion für uns erstellt. Dieses Modul gibt Ihnen ein Template zum Schreiben Ihrer Tests, sodass Sie nicht jedes Mal die genaue Struktur und Syntax recherchieren müssen, wenn Sie ein neues Projekt starten. Sie können so viele zusätzliche Testfunktionen und so viele Testmodule hinzufügen, wie Sie möchten!

Wir werden einige Aspekte der Funktionsweise von Tests erkunden, indem wir mit der Vorlage-Test experimentieren, bevor wir tatsächlich Code testen. Dann werden wir einige echte Tests schreiben, die auf Code zugreifen, den wir geschrieben haben, und überprüfen, ob sein Verhalten korrekt ist.

Lassen Sie 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 Ihrer adder-Bibliothek sollte wie in Listing 11-1 aussehen.

Dateiname: src/lib.rs

#[cfg(test)]
mod tests {
  1 #[test]
    fn it_works() {
        let result = 2 + 2;
      2 assert_eq!(result, 4);
    }
}

Listing 11-1: Das automatisch von cargo new generierte Testmodul und -funktion

Lassen Sie uns für jetzt die obersten beiden Zeilen außer Acht und uns auf die Funktion konzentrieren. Beachten Sie die #[test]-Annotation [1]: Dieses Attribut gibt an, dass es sich um eine Testfunktion handelt, sodass der Testrunner weiß, diese Funktion als Test zu behandeln. Wir könnten auch nicht-Testfunktionen im tests-Modul haben, um allgemeine Szenarien einzurichten oder allgemeine Operationen durchzuführen, daher müssen wir immer angeben, welche Funktionen Tests sind.

Der Beispiel-Funktionskörper verwendet die assert_eq!-Makro [2], um zu überprüfen, dass result, das das Ergebnis der Addition von 2 und 2 enthält, gleich 4 ist. Diese Behauptung dient als Beispiel für das Format eines typischen Tests. Lassen Sie uns es ausführen, um zu sehen, dass dieser Test erfolgreich abgeschlossen wird.

Der Befehl cargo test führt alle Tests in unserem Projekt aus, wie in Listing 11-2 gezeigt.

[object Object]

Listing 11-2: Die Ausgabe bei der Ausführung des automatisch generierten Tests

Cargo hat die Tests kompiliert und ausgeführt. Wir sehen die Zeile running 1 test [1]. Die nächste Zeile zeigt den Namen der generierten Testfunktion, die it_works heißt, und dass das Ergebnis der Ausführung dieses Tests ok ist [2]. Die Gesamtübersicht test result: ok. [3] bedeutet, dass alle Tests erfolgreich abgeschlossen wurden, und der Teil, der 1 passed; 0 failed liest, summiert die Anzahl der Tests, die erfolgreich abgeschlossen oder fehlgeschlagen sind.

Es ist möglich, einen Test als ignoriert zu markieren, sodass er in einem bestimmten Fall nicht ausgeführt wird; wir werden das in "Ignoring Some Tests Unless Specifically Requested" behandeln. Da wir das hier nicht getan haben, zeigt die Zusammenfassung 0 ignored. Wir können auch einen Argument an den Befehl cargo test übergeben, um nur Tests auszuführen, deren Name einem String entspricht; dies wird als Filterung bezeichnet, und wir werden es in "Running a Subset of Tests by Name" behandeln. Hier haben wir die ausgeführten Tests nicht gefiltert, daher zeigt das Ende der Zusammenfassung 0 filtered out.

Die Statistik 0 measured ist für Benchmark-Tests, die die Leistung messen. Benchmark-Tests sind wie beim Schreiben dieses Dokuments nur in der nightly-Version von Rust verfügbar. Siehe die Dokumentation zu Benchmark-Tests unter https://doc.rust-lang.org/unstable-book/library-features/test.html, um mehr zu erfahren.

Der nächste Teil der Testausgabe, der bei Doc-tests adder beginnt [4], bezieht sich auf die Ergebnisse beliebiger Dokumentationstests. Wir haben noch keine Dokumentationstests, aber Rust kann alle Codebeispiele kompilieren, die in unserer API-Dokumentation auftauchen. Diese Funktion hilft, Ihre Dokumentation und Ihren Code in Sync zu halten! Wir werden diskutieren, wie man Dokumentationstests schreibt, in "Documentation Comments as Tests". Für jetzt werden wir die Doc-tests-Ausgabe außer Acht lassen.

Lassen Sie uns den Test an unsere eigenen Bedürfnisse anpassen. Ändern Sie zunächst den Namen der it_works-Funktion in einen anderen Namen, wie exploration, so:

Dateiname: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

Führen Sie dann erneut cargo test aus. Die Ausgabe zeigt jetzt exploration anstelle von it_works:

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

Jetzt fügen wir einen weiteren Test hinzu, aber diesmal einen Test, der fehlschlägt! Tests scheitern, wenn etwas in der Testfunktion einen Fehler auslöst. Jeder Test wird in einem neuen Thread ausgeführt, und wenn der Hauptthread sieht, dass ein Testthread abgestürzt ist, wird der Test als fehlgeschlagen markiert. Im Kapitel 9 haben wir darüber gesprochen, dass die einfachste Möglichkeit, einen Fehler auszulösen, der Aufruf der panic!-Makro ist. Geben Sie den neuen Test als Funktion namens another ein, sodass Ihre src/lib.rs-Datei wie in Listing 11-3 aussieht.

Dateiname: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

Listing 11-3: Hinzufügen eines zweiten Tests, der fehlschlägt, weil wir das panic!-Makro aufrufen

Führen Sie die Tests erneut mit cargo test aus. Die Ausgabe sollte wie in Listing 11-4 aussehen, was zeigt, dass unser exploration-Test erfolgreich abgeschlossen und another fehlgeschlagen ist.

running 2 tests
test tests::exploration... ok
1 test tests::another... FAILED

2 failures:

---- tests::another stdout ----
thread'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

3 failures:
    tests::another

4 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'

Listing 11-4: Testresultate, wenn ein Test erfolgreich abgeschlossen und ein Test fehlschlägt

Anstelle von ok zeigt die Zeile test tests::another FAILED [1]. Zwei neue Abschnitte erscheinen zwischen den einzelnen Ergebnissen und der Zusammenfassung: Der erste [2] zeigt die detaillierten Gründe für jeden Testfehler an. In diesem Fall erhalten wir die Details, dass another fehlgeschlagen ist, weil es panicked at 'Make this test fail' in Zeile 10 in der src/lib.rs-Datei. Der nächste Abschnitt [3] listet nur die Namen aller fehlgeschlagenen Tests auf, was hilfreich ist, wenn es viele Tests und viel detaillierte fehlende Testausgabe gibt. Wir können den Namen eines fehlgeschlagenen Tests verwenden, um nur diesen Test auszuführen, um ihn leichter zu debuggen; wir werden mehr über Möglichkeiten zur Ausführung von Tests in "Controlling How Tests Are Run" sprechen.

Die Zusammenfassungzeile wird am Ende angezeigt [4]: Insgesamt ist unser Testresultat FAILED. Wir hatten einen Test, der erfolgreich abgeschlossen wurde, und einen Test, der fehlgeschlagen ist.

Jetzt, nachdem Sie gesehen haben, wie die Testresultate in verschiedenen Szenarien aussehen, werden wir uns einige Makros außer panic! ansehen, die in Tests nützlich sind.

Überprüfen von Ergebnissen mit dem assert!-Makro

Das assert!-Makro, das von der Standardbibliothek bereitgestellt wird, ist nützlich, wenn Sie sicherstellen möchten, dass eine bestimmte Bedingung in einem Test true auswertet. Wir geben dem assert!-Makro einen Ausdruck, der zu einem Boolean ausgewertet wird. Wenn der Wert true ist, passiert nichts und der Test wird bestanden. Wenn der Wert false ist, ruft das assert!-Makro panic! auf, um den Test als fehlgeschlagen zu markieren. Das Verwenden des assert!-Makros hilft uns zu überprüfen, ob unser Code auf die von uns beabsichtigte Weise funktioniert.

In Listing 5-15 haben wir eine Rectangle-Struktur und eine can_hold-Methode verwendet, die hier in Listing 11-5 wiedergegeben sind. Lassen Sie uns diesen Code in die src/lib.rs-Datei einfügen und dann einige Tests dafür schreiben, indem wir das assert!-Makro verwenden.

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

Listing 11-5: Verwendung der Rectangle-Struktur und ihrer can_hold-Methode aus Kapitel 5

Die can_hold-Methode gibt einen Boolean zurück, was bedeutet, dass es ein perfektes Anwendungsfall für das assert!-Makro ist. In Listing 11-6 schreiben wir einen Test, der die can_hold-Methode testet, indem wir eine Rectangle-Instanz mit einer Breite von 8 und einer Höhe von 7 erstellen und überprüfen, dass sie eine andere Rectangle-Instanz mit einer Breite von 5 und einer Höhe von 1 aufnehmen kann.

Dateiname: src/lib.rs

#[cfg(test)]
mod tests {
  1 use super::*;

    #[test]
  2 fn larger_can_hold_smaller() {
      3 let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

      4 assert!(larger.can_hold(&smaller));
    }
}

Listing 11-6: Ein Test für can_hold, der überprüft, ob ein größerer Rechteck tatsächlich ein kleineres Rechteck aufnehmen kann

Beachten Sie, dass wir eine neue Zeile im tests-Modul hinzugefügt haben: use super::*; [1]. Das tests-Modul ist ein normales Modul, das den üblichen Sichtbarkeitsregeln folgt, die wir in "Paths for Referring to an Item in the Module Tree" behandelt haben. Da das tests-Modul ein inneres Modul ist, müssen wir den zu testenden Code im äußeren Modul in den Gültigkeitsbereich des inneren Moduls bringen. Wir verwenden hier ein Glob, sodass alles, was wir im äußeren Modul definieren, für dieses tests-Modul verfügbar ist.

Wir haben unseren Test larger_can_hold_smaller benannt [2], und wir haben die beiden erforderlichen Rectangle-Instanzen erstellt [3]. Dann haben wir das assert!-Makro aufgerufen und ihm das Ergebnis der Ausführung von larger.can_hold(&smaller) übergeben [4]. Dieser Ausdruck sollte true zurückgeben, sodass unser Test bestanden werden sollte. Finden wir heraus!

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

Es besteht tatsächlich! Lassen Sie uns einen weiteren Test hinzufügen, diesmal überprüfend, dass ein kleineres Rechteck kein größeres Rechteck aufnehmen kann:

Dateiname: src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        --snip--
    }

    #[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 can_hold-Funktion in diesem Fall false ist, müssen wir das Ergebnis verneinen, bevor wir es an das assert!-Makro übergeben. Dadurch wird unser Test bestehen, wenn can_hold false zurückgibt:

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

Zwei Tests, die bestehen! Jetzt sehen wir, was mit unseren Testresultaten passiert, wenn wir einen Fehler in unserem Code einführen. Wir ändern die Implementierung der can_hold-Methode, indem wir das größer-than-Zeichen durch ein kleiner-than-Zeichen ersetzen, wenn es die Breiten vergleicht:

--snip--

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

Das Ausführen der Tests liefert jetzt Folgendes:

running 2 tests
test tests::smaller_cannot_hold_larger... ok
test tests::larger_can_hold_smaller... FAILED

failures:

---- tests::larger_can_hold_smaller stdout ----
thread'main' panicked at 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:28:9
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

Unsere Tests haben den Fehler erkannt! Da larger.width 8 ist und smaller.width 5 ist, liefert der Vergleich der Breiten in can_hold jetzt false zurück: 8 ist nicht kleiner als 5.

Das Testen auf Gleichheit mit den assert_eq!- und assert_ne!-Makros

Ein üblicher Weg, um die Funktionalität zu überprüfen, ist es, die Gleichheit zwischen dem Ergebnis des zu testenden Codes und dem Wert zu testen, den Sie erwarten, dass der Code zurückgibt. Sie könnten dies tun, indem Sie das assert!-Makro verwenden und ihm einen Ausdruck mit dem ==-Operator übergeben. Allerdings ist dies ein so üblicher Test, dass die Standardbibliothek zwei Makros bereitstellt - assert_eq! und assert_ne! - um diesen Test komfortabler durchzuführen. Diese Makros vergleichen zwei Argumente auf Gleichheit oder Ungleichheit, respectively. Sie werden auch die beiden Werte ausgeben, wenn die Behauptung fehlschlägt, was es einfacher macht, zu sehen, warum der Test fehlgeschlagen ist; umgekehrt zeigt das assert!-Makro nur an, dass es einen false-Wert für den ==-Ausdruck erhalten hat, ohne die Werte auszugeben, die zu dem false-Wert geführt haben.

In Listing 11-7 schreiben wir eine Funktion namens add_two, die 2 zu ihrem Parameter addiert, und testen diese Funktion dann mit dem assert_eq!-Makro.

Dateiname: src/lib.rs

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }
}

Listing 11-7: Das Testen der Funktion add_two mit dem assert_eq!-Makro

Lassen Sie uns überprüfen, dass es besteht!

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

Wir übergeben 4 als Argument an assert_eq!, das gleich dem Ergebnis von add_two(2) ist. Die Zeile für diesen Test lautet test tests::it_adds_two... ok, und der Text ok zeigt an, dass unser Test bestanden wurde!

Lassen Sie uns einen Fehler in unserem Code einführen, um zu sehen, wie assert_eq! aussieht, wenn es fehlschlägt. Ändern Sie die Implementierung der add_two-Funktion, um stattdessen 3 hinzuzufügen:

pub fn add_two(a: i32) -> i32 {
    a + 3
}

Führen Sie die Tests erneut aus:

running 1 test
test tests::it_adds_two... FAILED

failures:

---- tests::it_adds_two stdout ----
1 thread'main' panicked at 'assertion failed: `(left == right)`
2   left: `4`,
3  right: `5`', src/lib.rs:11:9
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

Unser Test hat den Fehler erkannt! Der Test it_adds_two ist fehlgeschlagen, und die Fehlermeldung sagt uns, dass die fehlgeschlagene Behauptung assertion failed:(left == right)`[1] war und was dieleft[2] undright[3] Werte sind. Diese Meldung hilft uns, mit dem Debugging zu beginnen: dasleft-Argument war4, aber dasright-Argument, wo wiradd_two(2)hatten, war5`. Man kann sich vorstellen, dass dies besonders hilfreich wäre, wenn wir viele Tests ausführen.

Beachten Sie, dass in einigen Sprachen und Testframeworks die Parameter von Gleichheitsbehauptungsfunktionen expected und actual genannt werden und die Reihenfolge, in der wir die Argumente angeben, wichtig ist. In Rust werden sie jedoch left und right genannt, und die Reihenfolge, in der wir den Wert angeben, den wir erwarten, und den Wert, den der Code produziert, spielt keine Rolle. Wir könnten die Behauptung in diesem Test als assert_eq!(add_two(2), 4) schreiben, was zu derselben Fehlermeldung führen würde, die assertion failed:(left == right)`` anzeigt.

Das assert_ne!-Makro wird bestehen, wenn die beiden Werte, die wir ihm geben, nicht gleich sind, und fehlschlagen, wenn sie gleich sind. Dieses Makro ist am nützlichsten in Fällen, in denen wir uns nicht sicher sind, was ein Wert werden wird, aber wir wissen, was der Wert definitiv nicht sein sollte. Beispielsweise, wenn wir eine Funktion testen, die gewährleistet ist, ihren Input auf irgendeine Weise zu ändern, aber die Art, wie der Input geändert wird, von dem Tag der Woche abhängt, an dem wir unsere Tests ausführen, ist das Beste, was wir behaupten können, dass die Ausgabe der Funktion nicht gleich dem Input ist.

Im Hintergrund verwenden die assert_eq!- und assert_ne!-Makros die Operatoren == und != respectively. Wenn die Behauptungen fehlschlagen, geben diese Makros ihre Argumente mit Debug-Formatierung aus, was bedeutet, dass die zu vergleichenden Werte das PartialEq- und Debug-Trait implementieren müssen. Alle primitiven Typen und die meisten der Standardbibliothekstypen implementieren diese Traits. Für Structs und Enums, die Sie selbst definieren, müssen Sie PartialEq implementieren, um die Gleichheit dieser Typen zu beweisen. Sie müssen auch Debug implementieren, um die Werte auszugeben, wenn die Behauptung fehlschlägt. Da beide Traits ableitbare Traits sind, wie in Listing 5-12 erwähnt, ist dies normalerweise so einfach wie das Hinzufügen der #[derive(PartialEq, Debug)]-Annotation zu Ihrer Struct- oder Enum-Definition. Siehe Anhang C für weitere Details über diese und andere ableitbare Traits.

Hinzufügen benutzerdefinierter Fehlermeldungen

Sie können auch eine benutzerdefinierte Nachricht hinzufügen, die zusammen mit der Fehlermeldung als optionale Argumente an die assert!, assert_eq! und assert_ne!-Makros gedruckt wird. Alle nach den erforderlichen Argumenten angegebenen Argumente werden an das format!-Makro weitergeleitet (siehe "Concatenation with the + Operator or the format! Macro"), sodass Sie einen Formatstring übergeben können, der {}-Platzhalter enthält und die Werte, die in diese Platzhalter eingesetzt werden sollen. Benutzerdefinierte Nachrichten sind hilfreich, um zu dokumentieren, was eine Behauptung bedeutet; wenn ein Test fehlschlägt, haben Sie eine genauere Vorstellung davon, was das Problem mit dem Code ist.

Nehmen wir beispielsweise an, dass wir eine Funktion haben, die Menschen nach ihrem Namen begrüßt, und wir möchten testen, dass der Name, den wir in die Funktion übergeben, im Output erscheint:

Dateiname: src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {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, und wir sind ziemlich sicher, dass der Text Hello am Anfang der Begrüßung sich ändern wird. Wir haben beschlossen, nicht gezwungen zu sein, den Test zu aktualisieren, wenn sich die Anforderungen ändern, daher überprüfen wir statt der genauen Gleichheit mit dem von der greeting-Funktion zurückgegebenen Wert nicht, sondern stellen nur sicher, dass der Output den Text des Eingabeparameters enthält.

Lassen Sie uns nun einen Fehler in diesem Code einführen, indem wir greeting ändern, um name auszuschließen, um zu sehen, wie die standardmäßige Testfehlermeldung aussieht:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

Das Ausführen dieses Tests liefert Folgendes:

running 1 test
test tests::greeting_contains_name... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'assertion failed:
result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::greeting_contains_name

Dieses Ergebnis zeigt nur an, dass die Behauptung fehlgeschlagen ist und auf welcher Zeile die Behauptung steht. Eine nützlichere Fehlermeldung würde den Wert aus der greeting-Funktion ausgeben. Fügen wir eine benutzerdefinierte Fehlermeldung hinzu, die aus einem Formatstring besteht, in dem ein Platzhalter mit dem tatsächlichen Wert ersetzt ist, den wir von der greeting-Funktion erhalten haben:

#[test]
fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
        result.contains("Carol"),
        "Greeting did not contain name, value was `{result}`"
    );
}

Wenn wir jetzt den Test ausführen, erhalten wir eine informativere Fehlermeldung:

---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'Greeting did not contain name, value
was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

Wir können den Wert sehen, den wir tatsächlich im Testoutput erhalten haben, was uns helfen würde, das Problem zu debuggen, was passiert ist, anstatt was wir erwartet haben.

Überprüfen auf Panik mit should_panic

Neben der Überprüfung von Rückgabewerten ist es wichtig, zu überprüfen, ob unser Code Fehlerbedingungen wie erwartet behandelt. Beispielsweise betrachten wir den Guess-Typ, den wir in Listing 9-13 erstellt haben. Anderer Code, der Guess verwendet, setzt sich darauf verlassen, dass Guess-Instanzen nur Werte zwischen 1 und 100 enthalten. Wir können einen Test schreiben, der sicherstellt, dass das Versuchen, eine Guess-Instanz mit einem Wert außerhalb dieses Bereichs zu erstellen, einen Fehler auslöst.

Wir tun dies, indem wir das Attribut should_panic zu unserer Testfunktion hinzufügen. Der Test besteht, wenn der Code innerhalb der Funktion einen Fehler auslöst; der Test scheitert, wenn der Code innerhalb der Funktion keinen Fehler auslöst.

Listing 11-8 zeigt einen Test, der überprüft, ob die Fehlerbedingungen von Guess::new auftreten, wenn wir erwarten, dass sie es tun.

// src/lib.rs
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Listing 11-8: Testen, dass eine Bedingung einen Fehler auslöst!

Wir platzieren das Attribut #[should_panic] nach dem #[test]-Attribut und vor der Testfunktion, auf die es zutrifft. Schauen wir uns das Ergebnis an, wenn dieser Test besteht:

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

Es sieht gut aus! Jetzt fügen wir einen Fehler in unseren Code ein, indem wir die Bedingung entfernen, dass die new-Funktion einen Fehler auslöst, wenn der Wert größer als 100 ist:

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

Wenn wir den Test in Listing 11-8 ausführen, wird er fehlschlagen:

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

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 erhalten haben, bedeutet, dass der Code in der Testfunktion keinen Fehler ausgelöst hat.

Tests, die should_panic verwenden, können ungenau sein. Ein should_panic-Test würde bestehen, auch wenn der Test aus einem anderen Grund als dem, den wir erwarteten, einen Fehler auslöst. Um should_panic-Tests präziser zu machen, können wir einem optionalen expected-Parameter des should_panic-Attributs einen Wert hinzufügen. Der Testrunner wird sicherstellen, dass die Fehlermeldung den angegebenen Text enthält. Beispielsweise betrachten wir den modifizierten Code für Guess in Listing 11-9, in dem die new-Funktion je nachdem, ob der Wert zu klein oder zu groß ist, mit unterschiedlichen Nachrichten einen Fehler auslöst.

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Listing 11-9: Testen auf einen panic! mit einer Fehlermeldung, die einen bestimmten Teilstring enthält

Dieser Test wird bestehen, weil der Wert, den wir im expected-Parameter des should_panic-Attributs angegeben haben, ein Teilstring der Nachricht ist, mit der die Guess::new-Funktion einen Fehler auslöst. Wir hätten auch die gesamte erwartete Fehlermeldung angeben können, was in diesem Fall Guess value must be less than or equal to 100, got 200 wäre. Was Sie wählen, um anzugeben, hängt davon ab, wie viel der Fehlermeldung einzigartig oder dynamisch ist und wie genau Sie Ihren Test möchten. In diesem Fall ist ein Teilstring der Fehlermeldung ausreichend, um sicherzustellen, dass der Code in der Testfunktion den else if value > 100-Fall ausführt.

Um zu sehen, was passiert, wenn ein should_panic-Test mit einer expected-Nachricht fehlschlägt, fügen wir erneut einen Fehler in unseren Code ein, indem wir die Körper der if value < 1- und der else if value > 100-Blöcke tauschen:

// src/lib.rs
--snip--
if value < 1 {
    panic!(
        "Guess value must be less than or equal to 100, got {}.",
        value
    );
} else if value > 100 {
    panic!(
        "Guess value must be greater than or equal to 1, got {}.",
        value
    );
}
--snip--

Diesmal wird der should_panic-Test fehlschlagen, wenn wir ihn ausführen:

running 1 test
test tests::greater_than_100 - should panic... FAILED

failures:

---- tests::greater_than_100 stdout ----
thread'main' panicked at 'Guess value must be greater than or equal to 1, got
200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: `"Guess value must be greater than or equal to 1, got
200."`,
 expected substring: `"less than or equal to 100"`

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s

Die Fehlermeldung zeigt an, dass dieser Test tatsächlich wie erwartet einen Fehler ausgelöst hat, aber die Fehlermeldung enthielt nicht den erwarteten String 'Guess value must be less than or equal to 100'. Die Fehlermeldung, die wir in diesem Fall erhalten haben, war Guess value must be greater than or equal to 1, got 200. Jetzt können wir anfangen, herauszufinden, wo unser Fehler ist!

Verwenden von Result<T, E> in Tests

Bisher landen alle unsere Tests im Fehlerfall im Zustand eines Panik. Wir können auch Tests schreiben, die Result<T, E> verwenden! Hier ist der Test aus Listing 11-1, umgeschrieben, um Result<T, E> zu verwenden und einen Err zurückzugeben, anstatt einen Fehler auszulösen:

Dateiname: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

Die it_works-Funktion hat jetzt den Rückgabetyp Result<(), String>. Im Funktionskörper geben wir statt dem Aufruf des assert_eq!-Makros Ok(()) zurück, wenn der Test besteht, und einen Err mit einem String darin, wenn der Test fehlschlägt.

Das Schreiben von Tests, sodass sie einen Result<T, E> zurückgeben, ermöglicht es Ihnen, den Fragezeichen-Operator im Testkörper zu verwenden, was ein bequemer Weg sein kann, Tests zu schreiben, die fehlschlagen sollten, wenn eine beliebige Operation innerhalb von ihnen einen Err-Variant zurückgibt.

Sie können die #[should_panic]-Annotation nicht auf Tests verwenden, die Result<T, E> verwenden. Um zu überprüfen, dass eine Operation einen Err-Variant zurückgibt, verwenden Sie nicht den Fragezeichen-Operator auf dem Result<T, E>-Wert. Verwenden Sie stattdessen assert!(value.is_err()).

Jetzt, da Sie verschiedene Möglichkeiten kennen, Tests zu schreiben, schauen wir uns an, was passiert, wenn wir unsere Tests ausführen, und erkunden die verschiedenen Optionen, die wir mit cargo test verwenden können.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Wie man Tests schreibt" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.