Rust : tests automatisés
<syntaxhighlight lang='rust'> // Automated tests // Le principe d'un test est de faire 3 choses à la suite: // * Préparer un état / des données nécessaires // * Faire tourner le code à tester // * Vérifier les résultats // // Le principe le plus simple est d'ajouter la métadonnée: // #[test] // à une fonction, est de la tester avec cargo test. // // Lors de la création d'une lib, un module de test est généré // automatiquement.
//Reprenons notre struct Rectangle.
- [derive(Debug)]
struct Rectangle {
width: u32, height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }
}
//Fonction diverses utilisées plus tard //(Ce sont les fonctions que l'on teste) fn add_two(a: i32) -> i32 {
a + 2
}
pub fn greeting(name: &str) -> String {
format!("Hello {}!", name)
}
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess { if value < 1 { panic!( "Guess value must be greater than or equal to 1, got {}.", value ); } else if value > 100 { panic!( "Guess value must be less than or equal to 100, got {}.", value ); }
Guess { value } }
}
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {}", a); 10
}
//Partie tests
- [cfg(test)]
mod tests {
//Le module de tests est un module normal qui suit les règles //de visibilité. //Pour rappel, super rappelle des modules du parent. //On ramène tout le contenu du parent. use super::*;
//Cette metadata signifie au compilateur que c'est une fonction de test //Elle peut être accompagnée de fonctiosn non-test dont elle aurait //besoin #[test] fn exploration() { let result = 2 + 2; //assert_eq! sert à vérifier une égalité //assert_ne! sert à vérifier une non-égalité assert_eq!(result, 4); }
//Ce test échoue volontairement pour voir ce que ça fait //lors d'un cargo test. //#[test] //fn another() { // panic!("I was born by the failure, moulded by it"); //}
//On teste l'implémentation de notre struct Rectangle. //Notre impl can_hold renvoie un booléen, c'est parfait #[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; //assert! vérifie un booléen. assert!(larger.can_hold(&smaller)); }
//Autre test : on vérifie qu'un rectangle ne peut pas //contenir un autre rectangle plus grand. #[test] fn smaller_cant_hold_larger() { let larger = Rectangle { width : 20, height : 21 }; let smaller = Rectangle { width : 10, height : 15 };
assert!(!smaller.can_hold(&larger)); }
//Démonstration de assert_eq! //Les paramètre d'une assertion s'appellent left et right, //c'est bon à savoir pour comprendre les messages d'erreur //sur un test raté #[test] fn test_add_two() { assert_eq!(4, add_two(2)); }
//Démonstration de assert_ne! #[test] fn test_not_add_two() { assert_ne!(5, add_two(2)); } //Ce test échoue volontairement. //#[test] //fn greeting_contains_name() { // let result = greeting("Justine"); // //On peut ajouter un message en cas d'erreur. // //Le message est un format!. // assert!( // result.contains("Hi"), // "Greeting did not contain Hi, value was {}", // result // ); //}
//On peut vérifier une panique. //Ici, je m'attends à ce que le code panique. //J'aurais cette ligne dans le retour de cargo test: //test tests::greater_than_100 - should panic ... ok #[test] #[should_panic] fn greater_than_100() { Guess::new(200); }
//Un test de panique n'est pas forcément précis. //Je peux préciser quelle panique je dois récupérer. //Si ce n'est pas "less than or equal to 100", le test fail. #[test] #[should_panic(expected = "less than or equal to 100")] fn greater_than_100_precise() { Guess::new(200); }
//On peut aussi écrire un test qui utilise un Result. //On a alors pas accès au should_panic, mais on a accès au ? //dans nos tests. //Par défaut, si le test reçoit Ok, il est content. //Je pourrais ajouter assert!(mavaleur.is_err()) //si je m'attends à une erreur. #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("2 + 2 != 4, somehow")) } }
//Contrôler l'exécution des tests //Il existe plusieurs options, à la fois pour cargo test en lui-même //et pour le binaire généré par celui-ci. //Les options sont disposées comme suit: //cargo test <options de cargo> -- <options du binaire> // //Tests en parrallèle / consécutifs //Par défaut, cargo lance les tests en parrallèle sur divers threads. //Du coup, les tests ne doivent pas dépendre les uns des autres. //On peut changer ça comme ça: //cargo test -- --test-threads=1 // //Montrer les sorties de fonctions //Par défaut, les tests n'affichent rien sur la sortie standard; //un println! ne montrera rien. #[test] fn this_test_will_pass() { let value = prints_and_returns_10(4); assert_eq!(10, value); } //On peut afficher la sortie avec //cargo test -- --show-output //Lancer un ensemble de tests par un nom //On ne veut pas forcément lancer tous les tests; //on peut appeller unue fonction de test en particulier: //cargo test nom_fonction //On peut aussi utiliser un filtre pour lancer plusieurs fonctions. //De la même façon, on peut passer le début d'un nom de fonction; //toutes les fonctions qui matchent ce début de nom seront testées. //cargo test greater_than //On peut aussi ignorer des tests à moins de les appeller spécifiquement, //un peu comme un tag never dans Ansible. #[test] #[ignore] fn expensive_test() { // code that takes an hour to run } //...on peut alors appeller les tests ignorés: //cargo test -- --ignored //Ou tous les tests //cargo test -- --include-ignored
//Les tests en Rust sont généralement divisés en 2 types: //* Les tests unitaires sont sont petits et concentrés sur un //module à la fois //* Les tests d'intégration sont externes au code et utilisent le code //comme le ferait une librairie externe, en utilisant seulement les //interfaces publiques. //Les deux sont importants.
// Tests unitaires //Ils vont dans le répertoire src, dans le même fichier que le code //qu'ils servent à tester. La convention est d'avoir un module nommé tests //dans chacun de ces fichiers, et de l'annoter avec cfg(test).
//L'annotation #[cfg(test)] permet de faire tourner ce code seulement lors //de l'appel via cargo test, et pas build. On peut tester les //fonctions privées.
// Tests d'intégration //Comme dit, ils sont externes. Ils servent à savoir si notre lib va //bien fonctionner avec du code externe. //Ils vont dans un répertoire nommé tests, situé à côté de src. //J'en recopie un ici juste pour expliquer, mais il devrait aller dans //le répertoire tests/integration_test.rs; on suppose aussi que la présente //librairie s'appelle adder comme précisé dans Cargo.toml //Le présent fichier doit s'appeller lib.rs ! //use adder;
//#[test] //fn it_adds_two() { // assert_eq!(4, adder::add_two(2)); //} // //Je peux le lancer avec cargo test --test integration_test //A noter : pas besoin du #[cfg(test)] ici //Par ailleurs les tests d'intégration ne fonctionnent qu'avec //une lib crate, et pas une crate binaire avec un main.rs; //seule une lib expose des fonctions. // //Le plus courant est d'avoir une lib crate contenant //un lib.rs et un main.rs
//Submodules dans les test d'intégration //On peut d'ailleurs avoir des sous modules dans les tests d'intégration. //On peut faire plusieurs fichiers dans le répertoire tests; //contrairement à src, ils sont traités comme des crates différentes. //Par défaut tout est pris en compte par cargo test. // //Si on veut qu'un sous-module (qui par exemple ne sert que de helper //aux véritables modules de test) soit ignoré, on va créer // /tests/common/mod.rs //On aura donc: //├── Cargo.lock //├── Cargo.toml //├── src //│ └── lib.rs //└── tests // ├── common // │ └── mod.rs // └── integration_test.rs
}
</syntaxhighlight>