Rust Buch Lab: Einheitstests und Integrations-Tests

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 bei Test Organization. Dieses Labor ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Labor lernen wir über die zwei Hauptkategorien von Tests in der Rust-Community: Einheitstests, die klein und auf das isolierte Testen einzelner Module fokussiert sind, und Integrations-Tests, die die öffentliche Schnittstelle der Bibliothek verwenden und möglicherweise mehrere Module pro Test durchlaufen.

Testorganisation

Wie am Anfang des Kapitels erwähnt, ist das Testen eine komplexe Disziplin, und verschiedene Menschen verwenden unterschiedliche Terminologien und Organisationen. Die Rust-Community denkt sich Tests in zwei Hauptkategorien: Einheitstests und Integrations-Tests. Einheitstests sind klein und fokussierter, testen jeweils ein Modul isoliert und können private Schnittstellen testen. Integrations-Tests sind vollständig extern zu Ihrer Bibliothek und verwenden Ihren Code auf die gleiche Weise wie jeder andere externe Code, indem sie nur die öffentliche Schnittstelle verwenden und möglicherweise mehrere Module pro Test durchlaufen.

Es ist wichtig, beide Arten von Tests zu schreiben, um sicherzustellen, dass die Teile Ihrer Bibliothek das tun, was Sie von ihnen erwarten, separat und zusammen.

Einheitstests

Der Zweck von Einheitstests ist es, jede Codeeinheit isoliert von dem restlichen Code zu testen, um schnell zu ermitteln, wo der Code wie erwartet funktioniert und wo nicht. Du wirst Einheitstests im src-Verzeichnis in jeder Datei ablegen, in der sich der zu testende Code befindet. Die Konvention besteht darin, in jeder Datei ein Modul namens tests zu erstellen, um die Testfunktionen zu enthalten, und das Modul mit cfg(test) zu annotieren.

Das Tests-Modul und #[cfg(test)]

Die Annotation #[cfg(test)] auf dem tests-Modul sagt Rust, den Testcode nur zu kompilieren und auszuführen, wenn du cargo test ausführst, nicht wenn du cargo build ausführst. Dies spart Kompilierzeit, wenn du nur die Bibliothek bauen möchtest, und spart Platz im resultierenden kompilierten Artefakt, da die Tests nicht enthalten sind. Du wirst sehen, dass, weil Integrations-Tests in einem anderen Verzeichnis liegen, sie die #[cfg(test)]-Annotation nicht benötigen. Allerdings, weil Einheitstests in den gleichen Dateien wie der Code liegen, wirst du #[cfg(test)] verwenden, um anzugeben, dass sie nicht im kompilierten Ergebnis enthalten sein sollen.

Denke daran, dass Cargo uns diesen Code generiert hat, als wir das neue adder-Projekt im ersten Abschnitt dieses Kapitels erstellt haben:

Dateiname: src/lib.rs

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

Dieser Code ist das automatisch generierte tests-Modul. Das Attribut cfg steht für Konfiguration und sagt Rust, dass der folgende Codeabschnitt nur unter einer bestimmten Konfigurationseingabe enthalten sein soll. In diesem Fall ist die Konfigurationseingabe test, die von Rust für das Kompilieren und Ausführen von Tests bereitgestellt wird. Durch die Verwendung des cfg-Attributs kompiliert Cargo unseren Testcode nur, wenn wir die Tests aktiv mit cargo test ausführen. Dies umfasst alle Hilfsfunktionen, die möglicherweise innerhalb dieses Moduls sind, zusätzlich zu den Funktionen, die mit #[test] annotiert sind.

Das Testen von privaten Funktionen

Innerhalb der Testcommunity besteht eine Debatte darüber, ob private Funktionen direkt getestet werden sollten, und in anderen Sprachen ist es schwierig oder unmöglich, private Funktionen zu testen. Unabhängig davon, welchem Testideologie du folgst, erlauben Rusts Privatsphäre-Regeln es dir, private Funktionen zu testen. Betrachte den Code in Listing 11-12 mit der privaten Funktion internal_adder.

Dateiname: src/lib.rs

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

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

Listing 11-12: Das Testen einer privaten Funktion

Beachte, dass die Funktion internal_adder nicht als pub markiert ist. Tests sind einfach Rust-Code, und das tests-Modul ist einfach ein weiteres Modul. Wie wir in "Pfade für die Referenz auf ein Element im Modultree" diskutiert haben, können Elemente in Untermodulen die Elemente in ihren Vorfahrenmodulen verwenden. In diesem Test bringen wir alle Elemente des Elternmoduls des test-Moduls in den Geltungsbereich mit use super::*, und dann kann der Test internal_adder aufrufen. Wenn du denkst, dass private Funktionen nicht getestet werden sollten, zwingt dich nichts in Rust dazu, dies zu tun.

Integrations-Tests

In Rust sind Integrations-Tests vollständig extern zu Ihrer Bibliothek. Sie verwenden Ihre Bibliothek auf die gleiche Weise wie jeder andere Code, was bedeutet, dass sie nur Funktionen aufrufen können, die Teil der öffentlichen Schnittstelle Ihrer Bibliothek sind. Ihr Zweck ist es, zu testen, ob viele Teile Ihrer Bibliothek zusammen korrekt funktionieren. Einheiten von Code, die einzeln korrekt funktionieren, können Probleme haben, wenn sie integriert werden, daher ist auch die Testabdeckung des integrierten Codes wichtig. Um Integrations-Tests zu erstellen, benötigen Sie zunächst ein Verzeichnis tests.

Das tests-Verzeichnis

Wir erstellen ein Verzeichnis tests auf der obersten Ebene unseres Projektverzeichnisses, neben src. Cargo weiß, in diesem Verzeichnis nach Integrations-Testdateien zu suchen. Wir können dann so viele Testdateien wie wir möchten erstellen, und Cargo wird jede Datei als eigenständigen Kasten kompilieren.

Lassen Sie uns einen Integrations-Test erstellen. Mit dem Code in Listing 11-12 noch in der Datei src/lib.rs, erstellen Sie ein Verzeichnis tests und eine neue Datei namens tests/integration_test.rs. Ihre Verzeichnisstruktur sollte so aussehen:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

Geben Sie den Code in Listing 11-13 in die Datei tests/integration_test.rs ein.

Dateiname: tests/integration_test.rs

use adder;

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

Listing 11-13: Ein Integrations-Test einer Funktion im adder-Kasten

Jede Datei im tests-Verzeichnis ist ein separater Kasten, daher müssen wir unsere Bibliothek in den Geltungsbereich jedes Testkastens bringen. Aus diesem Grund fügen wir use adder; am Anfang des Codes hinzu, was wir in den Einheitstests nicht benötigten.

Wir müssen keinen Code in tests/integration_test.rs mit #[cfg(test)] annotieren. Cargo behandelt das tests-Verzeichnis speziell und kompiliert Dateien in diesem Verzeichnis nur, wenn wir cargo test ausführen. Führen Sie jetzt cargo test aus:

[object Object]

Die drei Abschnitte der Ausgabe umfassen die Einheitstests, den Integrations-Test und die Dokutests. Beachten Sie, dass, wenn ein Test in einem Abschnitt fehlschlägt, die folgenden Abschnitte nicht ausgeführt werden. Beispielsweise wird, wenn ein Einheitstest fehlschlägt, keine Ausgabe für Integrations- und Dokutests erscheinen, da diese Tests nur ausgeführt werden, wenn alle Einheitstests bestanden werden.

Der erste Abschnitt für die Einheitstests [1] ist der gleiche wie bisher: Eine Zeile für jeden Einheitstest (einer benannt internal, den wir in Listing 11-12 hinzugefügt haben) und dann eine Zusammenfassungszeile für die Einheitstests.

Der Abschnitt mit den Integrations-Tests beginnt mit der Zeile Running tests/integration_test.rs [2]. Danach gibt es eine Zeile für jede Testfunktion in diesem Integrations-Test [3] und eine Zusammenfassungszeile für die Ergebnisse des Integrations-Tests [4] direkt vor dem Abschnitt Doc-tests adder beginnt.

Jede Integrations-Testdatei hat ihren eigenen Abschnitt, daher wird, wenn wir weitere Dateien im tests-Verzeichnis hinzufügen, es auch mehr Abschnitte mit Integrations-Tests geben.

Wir können immer noch einen bestimmten Integrations-Testfunktion ausführen, indem wir den Namen der Testfunktion als Argument für cargo test angeben. Um alle Tests in einer bestimmten Integrations-Testdatei auszuführen, verwenden Sie das --test-Argument von cargo test gefolgt vom Dateinamen:

[object Object]

Dieser Befehl führt nur die Tests in der Datei tests/integration_test.rs aus.

Untermodule in Integrations-Tests

Wenn Sie mehr Integrations-Tests hinzufügen, möchten Sie möglicherweise mehr Dateien im tests-Verzeichnis erstellen, um sie zu organisieren; beispielsweise können Sie die Testfunktionen nach der Funktionalität gruppieren, die sie testen. Wie bereits erwähnt, wird jede Datei im tests-Verzeichnis als eigener separater Kasten kompiliert, was hilfreich ist, um separate Geltungsbereiche zu erstellen, um die Art und Weise zu näher imitieren, wie Endbenutzer Ihre Bibliothek verwenden werden. Dies bedeutet jedoch, dass Dateien im tests-Verzeichnis nicht das gleiche Verhalten wie Dateien in src haben, wie Sie im Kapitel 7 gelernt haben, wie man Code in Module und Dateien aufteilt.

Das unterschiedliche Verhalten von Dateien im tests-Verzeichnis ist am auffälligsten, wenn Sie eine Reihe von Hilfsfunktionen haben, die in mehreren Integrations-Testdateien verwendet werden sollen, und Sie versuchen, die Schritte in "Trennen von Modulen in verschiedene Dateien" zu folgen, um sie in ein gemeinsames Modul zu extrahieren. Beispielsweise, wenn wir tests/common.rs erstellen und eine Funktion namens setup darin platzieren, können wir einigen Code in setup hinzufügen, den wir von mehreren Testfunktionen in mehreren Testdateien aufrufen möchten:

Dateiname: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

Wenn wir die Tests erneut ausführen, sehen wir einen neuen Abschnitt in der Testausgabe für die Datei common.rs, obwohl diese Datei keine Testfunktionen enthält und wir die setup-Funktion auch von keinem anderen Ort aus aufgerufen haben:

running 1 test
test tests::internal... ok

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

     Running tests/common.rs (target/debug/deps/common-
92948b65e88960b4)

running 0 tests

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

     Running tests/integration_test.rs
(target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test 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

Dass common in den Testresultaten erscheint, mit running 0 tests für es angezeigt, ist nicht das, was wir wollten. Wir wollten nur etwas Code mit den anderen Integrations-Testdateien teilen. Um zu vermeiden, dass common in der Testausgabe erscheint, erstellen wir statt tests/common.rs tests/common/mod.rs anstelle. Das Projektverzeichnis sieht jetzt so aus:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

Dies ist die ältere Namenskonvention, die Rust ebenfalls versteht, die wir in "Alternative Dateipfade" erwähnt haben. Wenn wir die Datei so benennen, sagt Rust, die common-Modul nicht als Integrations-Testdatei zu behandeln. Wenn wir den Code der setup-Funktion in tests/common/mod.rs verschieben und die Datei tests/common.rs löschen, wird der Abschnitt in der Testausgabe nicht mehr erscheinen. Dateien in Unterverzeichnissen des tests-Verzeichnisses werden nicht als separate Kisten kompiliert oder haben Abschnitte in der Testausgabe.

Nachdem wir tests/common/mod.rs erstellt haben, können wir es von jeder der Integrations-Testdateien als Modul verwenden. Hier ist ein Beispiel, wie die setup-Funktion aus dem Test it_adds_two in tests/integration_test.rs aufgerufen wird:

Dateiname: tests/integration_test.rs

use adder;

mod common;

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

Beachte, dass die Deklaration mod common; die gleiche wie die Moduldeklaration ist, die wir in Listing 7-21 demonstriert haben. Dann können wir in der Testfunktion die Funktion common::setup() aufrufen.

Integrations-Tests für binäre Kisten

Wenn unser Projekt eine binäre Kiste ist, die nur eine Datei src/main.rs enthält und keine Datei src/lib.rs hat, können wir keine Integrations-Tests im tests-Verzeichnis erstellen und Funktionen, die in der Datei src/main.rs definiert sind, mit einer use-Anweisung in den Geltungsbereich bringen. Nur Bibliothekskisten stellen Funktionen zur Verfügung, die andere Kisten verwenden können; binäre Kisten sind dazu gedacht, einzeln ausgeführt zu werden.

Dies ist einer der Gründe, warum Rust-Projekte, die eine Binärdatei liefern, eine einfache src/main.rs-Datei haben, die Logik aufruft, die in der Datei src/lib.rs enthalten ist. Mit dieser Struktur können Integrations-Tests die Bibliothekskiste mit use testen, um die wichtigen Funktionen verfügbar zu machen. Wenn die wichtigen Funktionen funktionieren, wird auch der kleine Code in der Datei src/main.rs funktionieren, und dieser kleine Code muss nicht getestet werden.

Zusammenfassung

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