Steuern wie Tests ausgeführt werden

So wie cargo run deinen Code kompiliert und dann die resultierende Binärdatei ausführt, kompiliert cargo test deinen Code im Testmodus und führt die resultierende Testbinärdatei aus. Das Standardverhalten der von cargo test erzeugten Binärdatei besteht darin, alle Tests parallel auszuführen und die während der Testläufe generierte Ausgabe zu erfassen, wodurch verhindert wird, dass die Ausgabe angezeigt wird, und das Lesen der Ausgabe bezüglich der Testergebnisse erleichtert wird. Du kannst jedoch Kommandozeilen-Optionen angeben, um dieses Standardverhalten zu ändern.

Einige Kommandozeilen-Optionen betreffen cargo test und einige betreffen die resultierende Testbinärdatei. Um diese beiden Argumentarten auseinanderzuhalten, gibst du zuerst die Argumente für cargo test, gefolgt vom Trennzeichen --, und danach die der Testbinärdatei an. Wenn du cargo test --help ausführst, werden die Optionen angezeigt, die du für cargo test verwenden kannst, und wenn du cargo test -- --help ausführst, werden die Optionen angezeigt, die du nach dem Trennzeichen verwenden kannst.

Tests parallel oder nacheinander ausführen

Wenn du mehrere Tests ausführst, werden diese standardmäßig parallel in Strängen (threads) ausgeführt, das bedeutet, dass die Tests schneller abgeschlossen werden und du schneller Rückmeldung erhältst. Da die Tests gleichzeitig ausgeführt werden, musst du sicherstellen, dass deine Tests nicht voneinander oder von einem gemeinsam genutzten Zustand abhängen, einschließlich einer gemeinsam genutzten Umgebung, z.B. dem aktuellen Arbeitsverzeichnis oder Umgebungsvariablen.

Angenommen, jeder deiner Tests führt einen Code aus, der eine Datei auf der Festplatte mit dem Namen test-output.txt erstellt und einige Daten in diese Datei schreibt. Dann liest jeder Test Daten aus dieser Datei und stellt fest, dass die Datei einen bestimmten Wert enthält, der bei jedem Test anders ist. Da die Tests zur gleichen Zeit laufen, kann es vorkommen, dass ein Test die Datei überschreibt, während ein anderer Test die Datei schreibt und liest. Der zweite Test wird dann fehlschlagen, nicht weil der Code falsch ist, sondern weil sich die Tests bei der parallelen Ausführung gegenseitig beeinflusst haben. Eine Lösung besteht darin, dafür zu sorgen, dass jeder Test in eine eigene Datei schreibt; eine andere Lösung besteht darin, die Tests einzeln nacheinander auszuführen.

Wenn du die Tests nicht parallel ausführen möchtest oder wenn du eine feingranularere Kontrolle über die Anzahl der verwendeten Stränge haben willst, kannst du den Schalter --test-threads mit der Anzahl der Stränge, die du verwenden möchtest, an die Testbinärdatei übergeben. Sieh dir das folgende Beispiel an:

$ cargo test -- --test-threads=1

Wir setzen die Anzahl der Teststränge auf 1 und weisen das Programm an, keine Parallelität zu verwenden. Die Ausführung der Tests mit einem Strang dauert länger als die parallele Ausführung, aber die Tests stören sich nicht gegenseitig, wenn sie den gleichen Zustand verwenden.

Anzeigen der Funktionsausgabe

Standardmäßig erfasst die Testbibliothek von Rust bei einem bestandenen Test alles, was in die Standardausgabe ausgegeben wurde. Wenn wir beispielsweise println! in einem Test aufrufen und der Test erfolgreich ist, sehen wir die Ausgabe von println! im Terminal nicht; wir sehen nur die Zeile, die den bestandenen Test anzeigt. Wenn ein Test fehlschlägt, sehen wir das, was in die Standardausgabe ausgegeben wurde, mit dem Rest der Fehlermeldung.

Als Beispiel hat Codebock 11-10 eine dumme Funktion, die den Wert ihres Parameters ausgibt und 10 zurückgibt, sowie einen Test, der bestanden wird, und einen Test, der fehlschlägt.

Dateiname: src/lib.rs

fn prints_and_returns_10(a: i32) -> i32 {
    println!("Ich habe den Wert {a} erhalten.");
    10
}

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

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(value, 10);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(value, 5);
    }
}

Codebock 11-10: Tests einer Funktion, die println! aufruft

Wenn wir diese Tests mit cargo test ausführen, werden wir folgende Ausgabe sehen:

$ cargo test
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

failures:

---- tests::this_test_will_fail stdout ----
Ich habe den Wert 8 erhalten.
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

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`

Beachte, dass wir nirgendwo in dieser Ausgabe Ich habe den Wert 4 erhalten. sehen, was beim erfolgreichen Testlauf ausgegeben wird. Diese Ausgabe wurde aufgefangen. Die Ausgabe Ich habe den Wert 8 erhalten. des fehlgeschlagenen Tests erscheint im Abschnitt der Testzusammenfassung, der auch die Ursache des Testfehlers anzeigt.

Wenn wir auch die ausgegebenen Werte der bestandenen Tests sehen wollen, können wir Rust mit --show-output anweisen, die Ausgabe erfolgreicher Tests mit anzuzeigen:

$ cargo test -- --show-output

Wenn wir die Tests in Codeblock 11-10 mit dem Schalter --show-output erneut ausführen, sehen wir folgende Ausgabe:

$ cargo test -- --show-output
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished test [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

successes:

---- tests::this_test_will_pass stdout ----
Ich habe den Wert 4 erhalten.


successes:
    tests::this_test_will_pass

failures:

---- tests::this_test_will_fail stdout ----
Ich habe den Wert 8 erhalten.
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

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`

Ausführen einer Test-Teilmenge mittels Name

Manchmal kann die Ausführung einer vollständigen Testsammlung sehr lange dauern. Wenn du an Code in einem bestimmten Bereich arbeitest, solltest du vielleicht nur die Tests ausführen, die diesen Code betreffen. Du kannst wählen, welche Tests ausgeführt werden sollen, indem du cargo test den oder die Namen der Tests, die du ausführen willst, als Argument übergibst.

Um zu demonstrieren, wie man eine Teilmenge von Tests ausführt, werden wir zuerst drei Tests für unsere Funktion add_two erstellen, wie in Codeblock 11-11 zu sehen ist, und auswählen, welche wir ausführen wollen.

Dateiname: src/lib.rs

#![allow(unused)]
fn main() {
pub fn add_two(a: usize) -> usize {
    a + 2
}

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

    #[test]
    fn add_two_and_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }

    #[test]
    fn add_three_and_two() {
        let result = add_two(3);
        assert_eq!(result, 5);
    }

    #[test]
    fn one_hundred() {
        let result = add_two(100);
        assert_eq!(result, 102);
    }
}
}

Codeblock 11-11: Drei Tests mit drei verschiedenen Namen

Wenn wir die Tests ohne Argumente durchführen, wie vorhin gesehen, werden alle Tests parallel laufen:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok

test result: ok. 3 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

Ausführen einzelner Tests

Wir können den Namen einer beliebigen Testfunktion an cargo test übergeben, um nur diesen Test auszuführen:

$ cargo test one_hundred
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.69s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::one_hundred ... ok

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

Nur der Test mit dem Namen one_hundred lief; die beiden anderen Tests passten nicht zu diesem Namen. Die Testausgabe lässt uns wissen, dass wir mehrere Tests hatten, als dieser Befehl ausgeführt wurde, indem am Ende der Zusammenfassungszeile 2 filtered out angezeigt wird.

Wir können die Namen mehrerer Tests nicht auf diese Weise angeben; es wird nur der erste Wert verwendet, der bei cargo test angegeben wird. Aber es gibt eine Möglichkeit, mehrere Tests auszuführen.

Filtern um mehrerer Tests auszuführen

Wir können einen Teil eines Testnamens angeben und jeder Test, dessen Name zu diesem Wert passt, wird ausgeführt. Da zum Beispiel zwei der Namen unserer Tests add enthalten, können wir diese beiden Tests ausführen, indem wir cargo test add ausführen:

$ cargo test add
   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 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

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

Dieser Befehl führte alle Tests mit add im Namen aus und filterte den Test mit dem Namen one_hundred heraus. Beachte auch, dass das Modul, in dem sich ein Test befindet, Teil des Testnamens wird, sodass wir alle Tests in einem Modul ausführen können, indem wir nach dem Namen des Moduls filtern.

Tests ignorieren, die nicht ausdrücklich verlangt werden

Manchmal kann die Ausführung einiger spezifischer Tests sehr zeitaufwendig sein, sodass du diese bei den meisten cargo test-Aufrufen ausschließen solltest. Anstatt alle Tests, die du ausführen möchtest, als Argumente aufzulisten, kannst du die zeitaufwendigen Tests stattdessen mit dem Attribut ignore annotieren, um sie auszuschließen, wie hier gezeigt:

Dateiname: src/lib.rs

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

    #[test]
    #[ignore]
    fn expensive_test() {
        // code that takes an hour to run
    }
}

Unterhalb #[test] fügen wir die Zeile #[ignore] beim Test ein, den wir ausschließen wollen. Wenn wir nun unsere Tests ausführen, läuft it_works, aber expensive_test nicht:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test expensive_test ... ignored
test it_works ... ok

test result: ok. 1 passed; 0 failed; 1 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

Die Funktion expensive_test wird als ignored aufgeführt. Wenn wir nur die ignorierten Tests ausführen wollen, können wir cargo test -- --ignored angeben:

$ cargo test -- --ignored
   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 expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 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

Indem du kontrollierst, welche Tests durchgeführt werden, kannst du sicherstellen, dass dein Aufruf von cargo test schnell Ergebnisse zurückgibt. Wenn du an einem Punkt angelangt bist, an dem es sinnvoll ist, die Ergebnisse der ignored-Tests zu überprüfen, und du Zeit hast, auf die Ergebnisse zu warten, kannst du stattdessen cargo test -- --ignored ausführen. Wenn du alle Tests ausführen willst, egal ob sie ignoriert werden oder nicht, kannst du cargo test -- --include-ignored ausführen.