La panique n'est pas une fatalité : pourquoi tester les situations d'urgence en Rust ?
\n\nImaginez : vous écrivez en Rust une bibliothèque pour gérer de l'argent. La fonction divide_money(amount: u64, parts: u64) doit paniquer si parts est égal à zéro — car on ne peut pas diviser par zéro, et vous n'avez pas voulu retourner Option ou Result pour des raisons architecturales. Comment vous assurer que cette panique se produit réellement ? Ou vous développez un analyseur syntaxique qui doit se terminer anormalement lors de la transmission de données incorrectes. Sans tests de panique, vous risquez d'avoir des bogues "silencieux" qui ne se manifesteront qu'en production.
En Rust, tester la panique n'est pas simplement une "option agréable", mais un outil important pour créer un code fiable. Dans cet article, nous allons voir comment écrire des tests unitaires qui vérifient que votre code panique vraiment dans les situations appropriées. Vous découvrirez la macro #[should_panic], ses nuances, des alternatives comme std::panic::catch_unwind et les meilleures pratiques. C'est parti !
Approche classique : la macro #[should_panic]
\n\nLa façon la plus simple et la plus courante de vérifier une panique en Rust est d'utiliser l'attribut #[should_panic]. Il se place au-dessus de la fonction de test et indique au compilateur : "Ce test n'est considéré comme réussi que si une panique s'est produite à l'intérieur".
Exemple de base
\n\n// lib.rs ou tests/unit_tests.rs\nfn divide(a: i32, b: i32) -> i32 {\n if b == 0 {\n panic!("Division par zéro !");\n }\n a / b\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n #[should_panic]\n fn test_divide_by_zero() {\n divide(10, 0);\n }\n}\n\nLancez les tests avec la commande cargo test — et vous verrez un résultat vert. Si la panique ne se produit pas (par exemple, si vous supprimez la vérification du zéro), le test échouera avec le message : "test test_divide_by_zero ... FAILED (expected to panic)".
Vérification du message de panique exact
\n\nParfois, il est nécessaire de vérifier non seulement le fait de la panique, mais aussi son message spécifique. Cela permet d'éviter les faux positifs, lorsqu'une panique se produit à cause d'une autre erreur. Utilisez le paramètre supplémentaire expected :
#[test]\n#[should_panic(expected = "Division par zéro !")]\nfn test_divide_by_zero_with_message() {\n divide(10, 0);\n}\n\n// Ce test-ci échouera — le message est différent\n#[test]\n#[should_panic(expected = "Division par zéro !")]\nfn test_other_panic() {\n panic!("Une erreur complètement différente");\n}\n\nImportant : expected vérifie que le message de panique contient la chaîne spécifiée (et non qu'il lui est exactement égal). C'est pratique pour les longs messages.
Approche avancée : std::panic::catch_unwind
\n\nParfois, #[should_panic] ne suffit pas. Par exemple, si vous voulez vérifier que la panique s'est produite à un endroit précis et non ailleurs, ou si vous devez tester du code asynchrone. Pour ces cas, il existe std::panic::catch_unwind — une fonction qui intercepte la panique et la retourne sous forme de Result.
Comment ça fonctionne
\n\nuse std::panic;\n\nfn risky_function(should_panic: bool) {\n if should_panic {\n panic!("Urgence !");\n }\n}\n\n#[test]\nfn test_catch_panic() {\n let result = panic::catch_unwind(|| {\n risky_function(true);\n });\n assert!(result.is_err()); // Vérifie qu'il y a eu une panique\n}\n\n#[test]\nfn test_no_panic() {\n let result = panic::catch_unwind(|| {\n risky_function(false);\n });\n assert!(result.is_ok()); // Pas de panique\n}\n\nVérification du type de panique
\n\nAvec catch_unwind, vous pouvez même vérifier que la panique contient une valeur spécifique (pas seulement une chaîne) :
use std::panic;\nuse std::any::Any;\n\nfn panic_with_number() {\n panic!(42); // Oui, c'est possible — une panique avec un nombre !\n}\n\n#[test]\nfn test_panic_with_number() {\n let resu