Rust : tests automatisés

De Justine's wiki
Aller à la navigation Aller à la recherche

<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.

  1. [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

  1. [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>