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>