Rust Bibliotheksfunktionalität mit Test-getriebener Entwicklung

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 Developing the Library’s Functionality With Test-Driven Development. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir die Funktionalität der Bibliothek unter Verwendung von Test-Driven Development entwickeln, um Suchlogik zum Programm hinzuzufügen.

Test-getriebene Entwicklung

Jetzt, da wir die Logik in src/lib.rs extrahiert haben und die Argumenteinsammlung und die Fehlerbehandlung in src/main.rs belassen haben, ist es viel einfacher, Tests für die Kernfunktionalität unseres Codes zu schreiben. Wir können Funktionen direkt mit verschiedenen Argumenten aufrufen und Rückgabewerte überprüfen, ohne dass wir unser Binärprogramm von der Befehlszeile aufrufen müssen.

In diesem Abschnitt fügen wir die Suchlogik zum minigrep-Programm hinzu, indem wir den Prozess der test-getriebenen Entwicklung (TDD) mit den folgenden Schritten anwenden:

  1. Schreiben Sie einen Test, der fehlschlägt, und führen Sie ihn aus, um sicherzustellen, dass er aus dem erwarteten Grund fehlschlägt.
  2. Schreiben Sie oder ändern Sie nur so viel Code, dass der neue Test erfolgreich ist.
  3. Optimieren Sie den gerade hinzugefügten oder geänderten Code und stellen Sie sicher, dass die Tests weiterhin erfolgreich sind.
  4. Wiederholen Sie ab Schritt 1!

Obwohl es nur eine von vielen Möglichkeiten ist, Software zu schreiben, kann die TDD die Codegestaltung unterstützen. Das Schreiben des Tests bevor Sie den Code schreiben, der den Test erfolgreich macht, hilft, eine hohe Testabdeckung während des gesamten Prozesses aufrechtzuerhalten.

Wir werden die Implementierung der Funktionalität test-getrieben entwickeln, die tatsächlich das Suchen nach der Abfragezeichenfolge in den Dateiinhalten durchführt und eine Liste von Zeilen erzeugt, die mit der Abfrage übereinstimmen. Wir werden diese Funktionalität in einer Funktion namens search hinzufügen.

Schreiben eines fehlschlagenden Tests

Da wir sie nicht mehr benötigen, entfernen wir die println!-Anweisungen aus src/lib.rs und src/main.rs, die wir früher verwendet haben, um das Verhalten des Programms zu überprüfen. Dann fügen wir in src/lib.rs ein tests-Modul mit einer Testfunktion hinzu, wie wir es im Kapitel 11 getan haben. Die Testfunktion definiert das Verhalten, das wir von der search-Funktion erwarten: Sie nimmt eine Abfrage und den Text, in dem gesucht werden soll, und gibt nur die Zeilen aus dem Text zurück, die die Abfrage enthalten. Listing 12-15 zeigt diesen Test, der noch nicht kompilieren wird.

Dateiname: src/lib.rs

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(
            vec!["safe, fast, productive."],
            search(query, contents)
        );
    }
}

Listing 12-15: Erstellen eines fehlschlagenden Tests für die search-Funktion, die wir haben möchten

Dieser Test sucht nach dem String "duct". Der Text, in dem wir suchen, besteht aus drei Zeilen, von denen nur eine "duct" enthält (beachten Sie, dass der Backslash nach der öffnenden Anführungszeichen Rust mitteilt, keine Zeilenumbrüche am Anfang des Inhalts dieses Stringliterals zu platzieren). Wir überprüfen, dass der von der search-Funktion zurückgegebene Wert nur die Zeile enthält, die wir erwarten.

Wir können diesen Test noch nicht ausführen und beobachten, wie er fehlschlägt, da der Test noch nicht einmal kompiliert: Die search-Funktion existiert noch nicht! In Übereinstimmung mit den TDD-Prinzipien fügen wir nur so viel Code hinzu, um den Test zu kompilieren und auszuführen, indem wir eine Definition der search-Funktion hinzufügen, die immer einen leeren Vektor zurückgibt, wie in Listing 12-16 gezeigt. Dann sollte der Test kompilieren und fehlschlagen, da ein leerer Vektor nicht mit einem Vektor übereinstimmt, der die Zeile "safe, fast, productive." enthält.

Dateiname: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    vec![]
}

Listing 12-16: Definieren Sie nur so viel der search-Funktion, dass Ihr Test kompiliert

Beachten Sie, dass wir in der Signatur von search ein explizites Lebenszeitparametername 'a definieren müssen und dieses Lebenszeitparametername mit dem contents-Argument und dem Rückgabewert verwenden. Erinnern Sie sich aus Kapitel 10, dass die Lebenszeitparameter bestimmen, welches Argumentlebenszeit mit der Lebenszeit des Rückgabewerts verbunden ist. Im Falle von search geben wir an, dass der zurückgegebene Vektor String-Slices enthalten soll, die Slices des contents-Arguments referenzieren (statt des query-Arguments).

Mit anderen Worten, wir sagen Rust, dass die Daten, die von der search-Funktion zurückgegeben werden, so lange leben wie die Daten, die als contents-Argument an die search-Funktion übergeben werden. Dies ist wichtig! Die Daten, auf die ein Slice verweist, müssen gültig sein, damit die Referenz gültig ist; wenn der Compiler annimmt, dass wir String-Slices von query statt von contents erstellen, wird er seine Sicherheitsüberprüfungen falsch durchführen.

Wenn wir die Lebenszeitangaben vergessen und versuchen, diese Funktion zu kompilieren, erhalten wir diesen Fehler:

error[E0106]: missing lifetime specifier
  --> src/lib.rs:31:10
   |
29 |     query: &str,
   |            ----
30 |     contents: &str,
   |               ----
31 | ) -> Vec<&str> {
   |          ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
   |
28 ~ pub fn search<'a>(
29 ~     query: &'a str,
30 ~     contents: &'a str,
31 ~ ) -> Vec<&'a str> {
   |

Rust kann nicht wissen, welches der beiden Argumente wir benötigen, daher müssen wir es ihm explizit sagen. Da contents das Argument ist, das alle unseren Text enthält und wir die Teile dieses Texts zurückgeben möchten, die übereinstimmen, wissen wir, dass contents das Argument ist, das mit dem Rückgabewert mithilfe der Lebenszeitsyntax verbunden werden sollte.

Andere Programmiersprachen erfordern es nicht, Argumente mit Rückgabewerten in der Signatur zu verbinden, aber diese Praxis wird mit der Zeit einfacher. Sie können diesen Beispiel mit den Beispielen in "Validating References with Lifetimes" vergleichen.

Lassen Sie uns jetzt den Test ausführen:

[object Object]

Super, der Test fehlschlägt, genau wie wir es erwartet haben. Lassen Sie uns den Test erfolgreich machen!

Schreiben von Code, um den Test zu bestehen

Derzeit fehlschlägt unser Test, weil wir immer einen leeren Vektor zurückgeben. Um das zu beheben und search zu implementieren, muss unser Programm die folgenden Schritte ausführen:

  1. Iterieren Sie über jede Zeile des Inhalts.
  2. Überprüfen Sie, ob die Zeile unseren Suchstring enthält.
  3. Wenn ja, fügen Sie sie zur Liste der zurückgegebenen Werte hinzu.
  4. Wenn nicht, tun Sie nichts.
  5. Geben Sie die Liste der übereinstimmenden Ergebnisse zurück.

Lassen Sie uns jeden Schritt einzeln durcharbeiten, beginnend mit dem Iterieren über die Zeilen.

Iterieren über Zeilen mit der lines-Methode

Rust hat eine hilfreiche Methode, um Zeilenweise-Iteration von Strings zu behandeln, die zweckmäßigerweise lines heißt und wie in Listing 12-17 funktioniert. Beachten Sie, dass dies noch nicht kompilieren wird.

Dateiname: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

Listing 12-17: Iterieren über jede Zeile in contents

Die lines-Methode gibt einen Iterator zurück. Wir werden uns im Kapitel 13 im Detail mit Iteratoren befassen, aber erinnern Sie sich daran, dass Sie diese Art des Verwenden eines Iterators in Listing 3-5 gesehen haben, wo wir eine for-Schleife mit einem Iterator verwendet haben, um einige Codezeilen auf jedes Element in einer Sammlung auszuführen.

Suchen in jeder Zeile nach der Abfrage

Als nächstes überprüfen wir, ob die aktuelle Zeile unseren Suchstring enthält. Glücklicherweise hat die String-Klasse eine hilfreiche Methode namens contains, die genau das für uns erledigt! Fügen Sie einen Aufruf der contains-Methode in die search-Funktion hinzu, wie in Listing 12-18 gezeigt. Beachten Sie, dass dies immer noch nicht kompilieren wird.

Dateiname: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

Listing 12-18: Hinzufügen der Funktionalität, um zu überprüfen, ob die Zeile den String in query enthält

Im Moment bauen wir die Funktionalität auf. Um den Code zu kompilieren, müssen wir einen Wert aus dem Funktionsrumpf zurückgeben, wie wir es in der Funktionssignatur angegeben haben.

Speichern von übereinstimmenden Zeilen

Um diese Funktion abzuschließen, benötigen wir eine Möglichkeit, die übereinstimmenden Zeilen zu speichern, die wir zurückgeben möchten. Dazu können wir ein mutables Vektor vor der for-Schleife erstellen und die push-Methode aufrufen, um eine line im Vektor zu speichern. Nach der for-Schleife geben wir den Vektor zurück, wie in Listing 12-19 gezeigt.

Dateiname: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

Listing 12-19: Speichern der übereinstimmenden Zeilen, damit wir sie zurückgeben können

Jetzt sollte die search-Funktion nur die Zeilen zurückgeben, die query enthalten, und unser Test sollte bestanden werden. Lassen Sie uns den Test ausführen:

$ cargo test
--snip--
running 1 test
test tests::one_result... ok

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

Unser Test ist bestanden, so dass wir wissen, dass es funktioniert!

An diesem Punkt könnten wir Möglichkeiten zur Umgestaltung der Implementierung der search-Funktion erwägen, während wir die Tests bestehen lassen, um die gleiche Funktionalität beizubehalten. Der Code in der search-Funktion ist nicht schlecht, aber er nutzt einige nützliche Funktionen von Iteratoren nicht aus. Wir werden in Kapitel 13 auf dieses Beispiel zurückkommen, wo wir Iteratoren im Detail untersuchen und sehen, wie wir es verbessern können.

Verwenden der search-Funktion in der run-Funktion

Jetzt, da die search-Funktion funktioniert und getestet ist, müssen wir search aus unserer run-Funktion aufrufen. Wir müssen den Wert config.query und den contents, den run aus der Datei liest, an die search-Funktion übergeben. Dann wird run jede Zeile aus search ausgeben:

Dateiname: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

Wir verwenden immer noch eine for-Schleife, um jede Zeile aus search zurückzugeben und auszugeben.

Jetzt sollte das gesamte Programm funktionieren! Probieren wir es aus, zunächst mit einem Wort, das genau eine Zeile aus dem Gedicht von Emily Dickinson zurückgeben sollte: frosch.

$ cargo run -- frosch gedicht.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frosch gedicht.txt`
Wie öffentlich, wie ein Frosch

Cool! Jetzt probieren wir ein Wort, das mehrere Zeilen übereinstimmen wird, wie Körper:

$ cargo run -- Körper gedicht.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep Körper gedicht.txt`
Ich bin Niemand! Wer bist du?
Bist du auch Niemand?
Wie langweilig, jemand zu sein!

Und schließlich stellen wir sicher, dass wir keine Zeilen erhalten, wenn wir nach einem Wort suchen, das nicht irgendwo im Gedicht vorkommt, wie Monomorphisierung:

$ cargo run -- Monomorphisierung gedicht.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep Monomorphisierung gedicht.txt`

Toller! Wir haben eine eigene Mini-Version eines klassischen Tools gebaut und viel über die Struktur von Anwendungen gelernt. Wir haben auch ein bisschen über Dateieingabe und -ausgabe, Lebenszeiten, Testen und Kommandozeilenanalyse gelernt.

Um dieses Projekt abzurunden, werden wir kurz demonstrieren, wie man mit Umgebungsvariablen umgeht und wie man auf die Standardfehlerausgabe schreibt, was beide nützlich sind, wenn man Kommandozeilenprogramme schreibt.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Entwicklung der Bibliotheksfunktionalität mit Test-getriebener Entwicklung" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.