Les tests en Rust
En Rust, les tests sont intégrés directement au langage et à cargo. Pas besoin d’installer une bibliothèque à part pour commencer. Cette page vous donne juste ce qu’il faut pour écrire et lancer vos premiers tests.
Écrire un test simple
Section intitulée « Écrire un test simple »Un test est une fonction ordinaire marquée avec #[test] :
fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }}Quelques points à remarquer :
#[cfg(test)]indique à Rust que ce module n’est compilé que lorsqu’on lance les tests. Il ne fait pas partie de votre programme normal.use super::*;importe tout ce que contient le module parent (ici, la fonctionadd), pour que le test puisse l’appeler.#[test]marque une fonction précise comme un test quecargo testdoit exécuter.
Lancer les tests
Section intitulée « Lancer les tests »Depuis le dossier de votre projet :
cargo testVous verrez une sortie de ce genre :
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredCargo écrit en anglais : running 1 test signifie « exécution d’un test », ok que le test a réussi, et la dernière ligne fait le bilan (1 passed : 1 réussi, 0 failed : 0 échoué, 0 ignored : 0 ignoré).
assert! et ses cousines
Section intitulée « assert! et ses cousines »Trois macros couvrent l’essentiel de vos besoins :
#[test]fn checks() { assert!(2 + 2 == 4); // fait échouer le test si la condition est fausse assert_eq!(2 + 2, 4); // échoue si les deux valeurs sont différentes assert_ne!(2 + 2, 5); // échoue si les deux valeurs SONT égales}On préfère en général assert_eq! et assert_ne! à un simple assert!(a == b). Quand elles échouent, elles affichent les deux valeurs : vous voyez tout de suite ce qui ne correspond pas, au lieu d’un simple « assertion failed » (l’assertion a échoué).
Un test qui échoue, et à quoi ça ressemble
Section intitulée « Un test qui échoue, et à quoi ça ressemble »fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn this_will_fail() { assert_eq!(add(2, 2), 5); }}test tests::this_will_fail ... FAILED
assertion `left == right` failed left: 4 right: 5C’est exactement le genre de message qu’on veut voir quand un test échoue. left (gauche) est la valeur que votre code a réellement produite, et right (droite) celle que le test attendait.
Tester qu’une fonction déclenche un panic
Section intitulée « Tester qu’une fonction déclenche un panic »Parfois, le bon comportement, c’est un panic (arrêt brutal du programme) : par exemple, une fonction qui doit refuser une entrée invalide. #[should_panic] sert à tester ce cas :
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("impossible de diviser par zéro"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}De meilleurs messages d’échec
Section intitulée « De meilleurs messages d’échec »Quand un test échoue, vous voulez savoir pourquoi le plus vite possible. Deux petits ajouts aident beaucoup.
Un message personnalisé. assert!, assert_eq! et assert_ne! acceptent toutes un message supplémentaire après les valeurs, écrit comme dans un println!. Il ne s’affiche que si la vérification échoue.
expected sur should_panic. Un #[should_panic] tout seul réussit pour n’importe quel panic, même s’il vient d’un autre bug. En ajoutant expected = "...", le test ne réussit que si le message du panic contient ce texte.
Voici de nouveau la fonction divide, avec un main pour pouvoir la lancer, et deux tests qui utilisent ces deux idées :
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("impossible de diviser par zéro"); } a / b}
fn main() { println!("10 / 2 = {}", divide(10, 2)); println!("7 / 2 = {}", divide(7, 2));}
#[cfg(test)]mod tests { use super::*;
#[test] fn rounds_down() { let result = divide(7, 2); assert_eq!(result, 3, "7 / 2 devrait être arrondi vers le bas, mais on obtient {result}"); }
#[test] #[should_panic(expected = "par zéro")] fn dividing_by_zero_panics() { divide(10, 0); }}Sortie :
10 / 2 = 57 / 2 = 3La sortie ci-dessus est ce qu’affiche cargo run, car main ne lance pas les tests. Pour lancer les tests, utilisez cargo test. Dans le Rust Playground, ouvrez le menu à côté de Run et choisissez Test. Vous verrez :
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outLes tests s’exécutent en parallèle : les deux lignes test ... peuvent donc apparaître dans n’importe quel ordre. Imaginez maintenant que le test attende 4 au lieu de 3. L’échec afficherait votre message, juste à côté des deux valeurs :
assertion `left == right` failed: 7 / 2 devrait être arrondi vers le bas, mais on obtient 3 left: 3 right: 4Et si divide déclenchait un panic avec un autre message, la vérification expected le repérerait :
note: panic did not contain expected string panic message: "impossible de diviser par zéro" expected substring: "négatif"Rust vous dit ici que le message du panic (panic message) ne contenait pas le texte attendu (expected substring).
Pourquoi écrire des tests ?
Section intitulée « Pourquoi écrire des tests ? »Pour être honnête : pour un petit script, vous ne le ferez sans doute pas. Les tests deviennent rentables dès que le code va être réutilisé ou modifié plus tard. Ils vous permettent de remanier votre code en toute confiance, car cargo test vous signale immédiatement si vous avez cassé quelque chose, au lieu que ce soit un utilisateur qui le découvre.
Les tests peuvent aussi vivre dans votre documentation : les exemples de code placés dans les commentaires de documentation /// sont eux aussi exécutés par cargo test, si bien que vos exemples ne peuvent jamais devenir obsolètes. La page Documentation en Rust vous montre comment faire.
Exercices
Section intitulée « Exercices »Pour les deux exercices, la solution contient un main que vous pouvez lancer, et des tests que vous lancez avec cargo test (ou Test dans le Playground).
1. Tester une fonction de notation
Section intitulée « 1. Tester une fonction de notation »Écrivez fn grade(score: u32) -> char, qui renvoie 'A' à partir de 90, 'B' de 80 à 89, 'C' de 70 à 79, et 'F' en dessous. Écrivez ensuite des tests qui vérifient les limites, comme 90 et 89. Indice : les bugs se cachent aux limites. Un test avec exactement 90 repère l’erreur classique qui consiste à écrire > au lieu de >=.
Voir la solution
fn grade(score: u32) -> char { if score >= 90 { 'A' } else if score >= 80 { 'B' } else if score >= 70 { 'C' } else { 'F' }}
fn main() { for score in [95, 85, 72, 40] { println!("{score} -> {}", grade(score)); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn edges_of_a() { assert_eq!(grade(90), 'A'); assert_eq!(grade(89), 'B'); }
#[test] fn edges_of_c() { assert_eq!(grade(70), 'C'); assert_eq!(grade(69), 'F'); }
#[test] fn zero_is_f() { assert_eq!(grade(0), 'F', "une note de 0 devrait être un échec"); }}Sortie :
95 -> A85 -> B72 -> C40 -> Fcargo test doit indiquer 3 passed (3 réussis). Essayez de remplacer score >= 90 par score > 90 et relancez les tests : edges_of_a échoue et vous montre left: 'B' et right: 'A'.
2. Tester une fonction qui renvoie une Option
Section intitulée « 2. Tester une fonction qui renvoie une Option »Écrivez fn safe_divide(a: i32, b: i32) -> Option<i32>, qui renvoie None quand b vaut 0 au lieu de déclencher un panic. Écrivez un test pour une division normale et un autre pour la division par zéro. Indice : assert_eq! fonctionne aussi avec Option, vous pouvez donc écrire assert_eq!(safe_divide(1, 0), None).
Voir la solution
fn safe_divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None } else { Some(a / b) }}
fn main() { println!("{:?}", safe_divide(10, 2)); println!("{:?}", safe_divide(1, 0));}
#[cfg(test)]mod tests { use super::*;
#[test] fn divides_normally() { assert_eq!(safe_divide(10, 2), Some(5)); }
#[test] fn zero_gives_none() { assert_eq!(safe_divide(1, 0), None); }}Sortie :
Some(5)NoneComparez avec la fonction divide plus haut. Là-bas, diviser par zéro provoque un panic, donc le test a besoin de #[should_panic]. Ici, c’est juste une valeur de retour normale : un simple assert_eq! suffit. Les fonctions qui renvoient une Option ou un Result sont en général plus faciles à tester.
Récapitulatif
Section intitulée « Récapitulatif »- Marquez une fonction avec
#[test]pour en faire un test ; lancez tous les tests aveccargo test. assert!,assert_eq!etassert_ne!couvrent la plupart des vérifications courantes. Préférezassert_eq!/assert_ne!pour des messages d’échec plus clairs.#[should_panic]vérifie qu’une fonction déclenche bien un panic quand elle le doit. Ajoutezexpected = "..."pour vérifier qu’elle l’a fait pour la bonne raison.- Ajoutez un message personnalisé à n’importe quelle macro d’assertion pour expliquer ce qui s’est mal passé en cas d’échec.
- Les tests sont surtout précieux pour le code que vous comptez modifier ou réutiliser plus tard.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quel attribut marque une fonction comme un test que cargo test doit exécuter ?
2. Pourquoi préfère-t-on en général assert_eq! et assert_ne! à un simple assert!(a == b) ?
3. À quoi sert #[should_panic] ?
Score : 0 / 3
Pour aller plus loin
Section intitulée « Pour aller plus loin »- How to Write Tests — The Rust Book (en anglais) :
#[test], les macros d’assertion, les messages personnalisés,should_panicet les tests qui renvoient unResult. - Controlling How Tests Are Run — The Rust Book (en anglais) : lancer un seul test par son nom, afficher la sortie de
println!, et#[ignore]. - Test Organization — The Rust Book (en anglais) : la différence entre les tests unitaires et les tests d’intégration placés dans un dossier
tests/. - Unit testing — Rust by Example (en anglais) : de courts exemples de tests qui réussissent, qui échouent et qui déclenchent un panic.
cargo test— The Cargo Book (en anglais) : toutes les options de la commandecargo test.