Wie schreibe ich einen Rust-Unit-Test, der sicherstellt, dass eine Panik aufgetreten ist?

74

Ich habe eine Rust-Funktion, die panicunter bestimmten Bedingungen funktioniert, und ich möchte einen Testfall schreiben, um zu überprüfen, ob die Funktion in Panik gerät oder nicht. Ich konnte nichts außer den Makros assert!und finden assert_eq!. Gibt es einen Mechanismus, um dies zu testen?

Ich könnte eine neue Aufgabe erzeugen und prüfen, ob diese Aufgabe in Panik gerät oder nicht. Macht das Sinn?


Die Rücksendung von a Result<T, E>ist in meinem Fall nicht geeignet.

Ich möchte Addeinem MatrixTyp, den ich implementiere , Unterstützung für das Merkmal hinzufügen . Die ideale Syntax für eine solche Addition würde folgendermaßen aussehen:

let m = m1 + m2 + m3;

wo m1, m2, m3sind alle Matrizen. Daher sollte der Ergebnistyp von addsein Matrix. So etwas wäre zu kryptisch:

let m = ((m1 + m2).unwrap() + m3).unwrap()

Gleichzeitig muss die add()Funktion überprüfen, ob die beiden hinzugefügten Matrizen dieselbe Dimension haben. Daher add()muss in Panik geraten, wenn die Abmessungen nicht übereinstimmen. Die verfügbare Option ist panic!().

Shailesh Kumar
quelle

Antworten:

108

Die Antwort finden Sie im Testabschnitt des Rust-Buches. Insbesondere möchten Sie #[should_panic]Attribut:

#[test]
#[should_panic]
fn test_invalid_matrices_multiplication() {
    let m1 = Matrix::new(3, 4);  // assume these are dimensions
    let m2 = Matrix::new(5, 6);
    m1 * m2
}
Vladimir Matveev
quelle
34
Es ist erwähnenswert, dass Sie einen Scheck für den Text der Panik hinzufügen können:#[should_panic(expected = "assertion failed")]
phss
1
Ein kurzer Hinweis hier, dass abhängig von Ihrer IDE und Umgebung die Stapelverfolgung der Panik möglicherweise immer noch in der Ausgabe angezeigt wird, der Test jedoch weiterhin bestanden wird. Es dauerte eine Minute, bis mir klar wurde, dass meine #[should_panic]tatsächlich funktionierte. Wenn Sie ein Generikum cargo testüber die Befehlszeile ausführen , werden Sie feststellen, dass es die Panik verschluckt und nur als angezeigt wird ok.
Ragona
40

Wie Francis Gagné in seiner Antwort erwähnt hat, finde ich das #[should_panic]Attribut auch für komplexere Tests nicht feinkörnig genug - wenn beispielsweise mein Testaufbau aus irgendeinem Grund fehlschlägt (dh ich habe einen schlechten Test geschrieben), tue ich dies Ich möchte, dass eine Panik als Misserfolg betrachtet wird!

Ab Rust 1.9.0 std::panic::catch_unwind()verfügbar. Es ermöglicht Ihnen, den Code, von dem Sie erwarten, dass er in Panik gerät, in einen Abschluss zu versetzen, und nur von diesem Code ausgegebene Panik wird als erwartet betrachtet (dh ein bestehender Test).

#[test]
fn test_something() {
    ... //<-- Any panics here will cause test failure (good)
    let result = std::panic::catch_unwind(|| <expected_to_panic_operation_here>);
    assert!(result.is_err());  //probe further for specific error type here, if desired
}

Beachten Sie, dass es keine nicht abwickelnde Panik auslösen kann (z std::process::abort(). B. ).

U007D
quelle
17

Wenn Sie behaupten möchten, dass nur ein bestimmter Teil der Testfunktion fehlschlägt, verwenden Sie std::panic::catch_unwind()und überprüfen Sie, ob ein ErrBeispiel zurückgegeben wird, z. B. mit is_err(). In komplexen Testfunktionen wird so sichergestellt, dass der Test nicht aufgrund eines frühen Fehlers fehlerhaft bestanden wird.

Mehrere Tests in der Rust-Standardbibliothek selbst verwenden diese Technik.

Francis Gagné
quelle
4
Sollte es dafür ein assert_failsoder ein assert_panicsMakro geben?
Dhardy
2
Sie können auch verwenden unwrap_err.
wimh
Gibt es eine Möglichkeit, dies in einer #[no_std]Umgebung zu tun ? Ich möchte ein assert_panics!Makro für den allgemeinen Gebrauch erstellen .
Jhpratt
4

Als Nachtrag: Die von @ U007D vorgeschlagene Lösung funktioniert auch in Doktrinen:

/// My identity function that panic for an input of 42.
///
/// ```
/// assert_eq!(my_crate::my_func(23), 23);
///
/// let result = std::panic::catch_unwind(|| my_crate::my_func(42));
/// assert!(result.is_err());
/// ```
pub fn my_func(input: u32) -> u32 {
    if input == 42 {
        panic!("Error message.");
    } else {
        input
    }
}
m00am
quelle
1

Verwenden Sie Folgendes catch_unwind_silentanstelle von regulär catch_unwind, um für erwartete Ausnahmen eine Stille in der Ausgabe zu erzielen:

use std::panic;

fn catch_unwind_silent<F: FnOnce() -> R + panic::UnwindSafe, R>(f: F) -> std::thread::Result<R> {
    let prev_hook = panic::take_hook();
    panic::set_hook(Box::new(|_| {}));
    let result = panic::catch_unwind(f);
    panic::set_hook(prev_hook);
    result
}
k06a
quelle