III. Aller plus loin
5. Ajouter des tests
Dans ce chapitre, nous allons parler des tests et en particulier de l'attribut #[test]
.
En Rust, il est possible d'écrire des tests unitaires directement dans un fichier qui peuvent être lancés par Cargo ou le compilateur de Rust.
Avec Cargo :
cargo test
Avec rustc :
rustc --test votre_fichier_principal.rs
./votre_fichier_principal
Regardons maintenant comment créer ces tests unitaires :
L'attribut #[test]
Pour indiquer au compilateur qu'une fonction est un test unitaire, il faut ajouter l'annoter avec l'attribut #[test]
. Exemple :
Runfn some_func(valeur1: i32, valeur2: i32) -> i32 {
valeur1 + valeur2
}
#[test]
fn test_some_func() {
assert_eq!(3, some_func(1, 2));
}
Et c'est tout... Il est courant de grouper les tests unitaires dans un module :
Runfn some_func(valeur1: i32, valeur2: i32) -> i32 {
valeur1 + valeur2
}
#[cfg(test)] // On ne compile ce module que si on est en mode "test".
mod tests {
use super::some_func;
#[test] // Cette fonction est donc un test unitaire.
fn test_some_func() {
assert_eq!(3, some_func(1, 2));
}
}
Ça permet de découper un peu le code.
La métadonnée #[should_panic]
Maintenant, si vous voulez vérifier qu'un test échoue, il vous faudra utiliser cet attribut :
Runfn some_func(valeur1: i32, valeur2: i32) -> i32 {
valeur1 + valeur2
}
#[test] // C'est un test.
#[should_panic] // Il est censé paniquer.
fn test_some_func() {
assert_eq!(4, some_func(1, 2)); // 1 + 2 != 4, donc ça doit paniquer.
}
Quand vous lancerez l'exécutable, il vous confirmera que le test s'est bien déroulé (parce qu'il a paniqué comme attendu). Petit bonus : vous pouvez ajouter du texte qui sera affiché lors de l'exécution du test :
Run#[test]
#[should_panic(expected = "1 + 2 != 4")]
fn test_some_func() {
assert_eq!(4, some_func(1, 2));
}
Mettre les tests dans un dossier à part
Si vous utilisez Cargo, il est aussi possible d'écrire des tests dans un dossier à part. Commencez par créer un dossier tests puis créez un fichier .rs:
Run#[test]
fn test_some_func() {
assert_eq!(3, ma_lib::some_func(1, 2));
}
Ensuite cela fonctionne de la même façon : lancez la commande cargo test
et les tests dans ce dossier seront exécutés.
Écrire des suites de tests
Si vous souhaitez regrouper plusieurs tests dans un même dossier (mais toujours dans le dossier tests), rien de bien difficile une fois encore. Ça devra ressembler à ça :
- tests
|
|- la_suite_de_tests.rs
|- sous_dossier
|
|- fichier1.rs
|- fichier2.rs
|- mod.rs
Je pense que vous voyez déjà où je veux en venir : il va juste falloir importer le module sous_dossier pour que les tests contenus dans fichier1.rs et fichier2.rs soient exécutés.
la_suite_de_tests.rs
Runmod sous_dossier; // Et c'est tout !
sous_dossier/mod.rs
Runmod fichier1;
mod fichier2;
Et voilà ! Vous pouvez maintenant écrire tous les tests que vous voulez dans fichier1.rs et fichier2.rs (en n'oubliant pas d'ajouter #[test]
!).
Tests dans la documentation ?
Comme évoqué dans le chapitre précédent, on peut ajouter des exemples de code dans la documentation. Ce que je ne vous avais pas dit, c'est que lorsque vous lancez cargo test
, ces exemples sont eux aussi testés. C'est très pratique car cela permet de les maintenir à jour assez facilement.
Options de test
Il est possible d'ajouter des options de test pour les codes d'exemple dans la documentation. Nous allons voir certains cas.
/// ```
/// let x = 12;
/// ```
C'est l'exemple de code par défaut. Si aucune option n'est passée, rustdoc partira donc du principe que c'est un code Rust et qu'il est censé compiler et s'exécuter sans paniquer.
Il est strictement équivalent au code suivant :
/// ```rust
/// let x = 12;
/// ```
Si vous voulez écrire du code dans un autre langage, écrivez juste son nom à la place de l'attribut rust :
/// ```C
/// int c = 12;
/// ```
Dans ce cas-là, ce code sera ignoré lors des tests.
Il se peut aussi que vous ayez envie d'ignorer un test :
/// ```ignore
/// let x = 12;
/// ```
Il sera marqué comme ignored mais vous le verrez lors des tests.
Un autre cas assez courant est de vouloir tester que la compilation se passe bien mais sans exécuter le code (généralement pour des exemples d'I/O) :
/// ```no_run
/// let x = File::open("Un-fichier.txt").expect("Fichier introuvable");
/// ```
Il est aussi possible de combiner plusieurs options en les séparant par une virgule :
/// ```compile_fail,no_run
/// let x = 12;
/// ```
Un dernier exemple :
```test_harness
#[test]
fn foo() {
fail!("oops! (will run & register as failure)")
}
```
Cela compile le code comme si le flag "--test" était donné au compilateur.
En bref, il y a pas mal d'options qui vous sont proposées dont voici la liste complète :
- rust : par défaut
- ignore : pour dire à rustdoc d'ignorer ce code
- should_panic : le test échouera si le code s'exécute sans erreur
- no_run : ne teste que la compilation
- test_harness : compile comme si le flag "--test" était donné au compilateur
- compile_fail : teste que la compilation échoue
- allow_fail : en gros, si l'exécution échoue, ça ne fera pas échouer le test. Par-contre le test doit compiler.
Tout autre option sera considérée comme un langage autre que Rust et passera le code en ignore invisible (vous ne le verrez pas apparaitre dans la liste des codes testés).
Cacher des lignes
Dans certains cas, vous pourriez vouloir cacher des lignes lors du rendu du code dans la documentation mais les garder lors du test. Exemple :
/// ```
/// # fn foo() -> io::Result<()> {
/// let f = File::open("un-fichier.txt")?;
/// # }
/// ```
Quand la doc sera générée, le lecteur ne verra plus que :
Runlet f = File::open("un-fichier.txt")?;
Par-contre, lors du lancement des tests, tout le code sera bien présent. Plutôt pratique si jamais vous avez besoin de concentrer l'attention du lecteur sur un point précis !