Utiliser les tests unitaires
En complément des exemples de la documentation destinée à aux utilisateurs·trices de notre API, nous pouvons intégrer des tests unitaires plus poussés dans nos codes sources.
Ici, par exemple, nous allons ajouter un test unitaire pour vérifier le comportements aux limites que j’ai survolé dans mon code en choisissant le type u64 en sortie de fonction.
Pour cela, nous allons ajouter un module de test à la suite de notre fonction. C’est la convention en Rust : nous disposons les tests unitaires dans le même fichier que le code source testé.
Code source pour implémenter le test
#![allow(unused)]
fn main() {
/// [Documentation avec exemple…]
pub fn calculer_perimetre(cote: u32) -> u64 {
(cote as u64) * 4_u64
}
#[cfg(test)]
mod tests {
use super::calculer_perimetre;
#[test]
fn test_limite_debordement() {
// Le nombre maximum pour un u32 est 4_294_967_295.
// Divisé par 4, cela donne 1_073_741_823.
// Si nous passons une longueur de côté supérieure à cette valeur,
// que se passe-t-il à l'exécution ?
let resultat = calculer_perimetre(1_073_741_824);
assert_eq!(resultat, 4_294_967_296);
}
}
}
Auto-critique sereine
Bien que robuste, le code que j’ai proposé pour sensibiliser à la réflexion sur les types et les conditions aux limites n’est pas top. Idem pour ce test qui n’a pas grand intérêt.
Dans des conditions plus réalistes, nous ajouterions un test pour vérifier que nous obtenons le bon résultat si nous passons en argument le plus grand entier u_32 à notre fonction (4_294_967_295).
Dans des conditions encore plus réalistes, nous travaillerions dans un espace de données cohérent (tout en u32 ou tout en u[…]), et :
- nous ajouterions des comportements dans notre fonction de calcul pour traiter les cas d’exceptions ;
- c’est sur la vérification du bon fonctionnement de ces traitements que nous écririons des tests unitaires.
Le test que nous avons écrit existerait (si nous choisissions de travailler en u32), mais cette fois pour nous assurer que la fonction retourne une erreur, et la bonne, plutôt qu’un résultat erroné.
Description détaillée
#[cfg(test)]
C’est un attribut de compilation conditionnelle.
cfgsignifie configuration.- Cet attribut indique au compilateur Rust que tout ce qui suit (ici, le module
tests) ne doit être compilé et exécuté que lorsque l’on lance la commandecargo test. - Lorsque nous compilons notre projet pour la production avec
cargo build --releasepar exemple, les blocs de tests sont ignorés. Cela évite d’alourdir le binaire final inutilement avec du code de tests.
mod tests { ... }
C’est la déclaration d’un sous-module nommé tests.
- En Rust, la convention veut que l’on isole les tests unitaires dans leur propre module.
- Ce module crée un nouvel espace de noms (namespace) étanche, ce qui évite de mélanger les fonctions de tests avec les fonctions de notre API.
use super::calculer_perimetre;
Nous importons notre fonction dans notre module de tests. super est un mot clef permettant de remonter au niveau supérieur (par rapport à notre sous-module de test).
#[test]
C’est l’attribut de test.
- Il sert de marqueur pour le gestionnaire de tests de Rust (test runner).
- Lorsqu’on tape
cargo test, Rust parcourt le code à la recherche de cet attribut et sait qu’il doit exécuter la fonction située juste en dessous.
fn test_limite_debordement() { ... }
C’est une fonction classique qui contient le scénario de test.
- Par convention, on lui donne un nom très descriptif (souvent préfixé par
test_) pour l’identifier dans la console qui affiche les résultats de tests. - Les fonctions de test ne prennent jamais de paramètres et ne retournent généralement rien.
let resultat = calculer_perimetre(1_073_741_824);
C’est l’action.
- On appelle la fonction à tester avec la valeur limite déterminée mathématiquement (
1_073_741_824). - Le résultat est stocké dans la variable
resultat.
assert_eq!(resultat, 4_294_967_296);
C’est la vérification (l’assertion).
assert_eq!est une macro (reconnaissable à son!) qui signifie assert equal (vérifier l’égalité).- Elle compare le premier argument (
resultat) avec le second argument attendu (4\_294\_967\_296). - Si les deux valeurs sont égales, le test passe. S’il y a la moindre différence ou si le code panique avant, le test échoue et Rust affiche explicitement la valeur obtenue face à la valeur attendue dans le retour.
Documentation
Comme pour l’ensemble de nos codes sources, nous prenons soin de documenter notre test en expliquant et explicitant tout ce qui doit l’être.
Nous le faisons avec des « commentaires » simples (//) : les tests, même s’ils complètent la documentation du code utilement pour les développeurs·ses et mainteneurs·ses de ce code, n’ont pas vocation à apparaître dans la documentation qui sert, elle, aux consommateurs·trices de notre API.
Nous pouvons remarquer et souligner que les tests unitaires constituent une part importante et pertinente de documentation des codes sources. Ils donnent des informations critiques (ici, le comportement aux limites).
Le fait que ces tests soient intégrés dans le code source AVEC ce code source me semble très utile : en tant que consommateur de codes écrits par d’autres, j’apprécie de pouvoir me faire une opinion des points d’attention que les développeurs·ses ont listés et traités (ou omis voire délaissés !) dans leurs tests unitaires.
Mise en application
Pour exécuter les tests, nous utilisons la commande que nous connaissons maintenant :
cargo test
La commande se charge de tout et retourne :
Compiling mon_api_maths v0.1.0 (/home/user/mesprojets/mon_api_maths)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.45s
Running unittests src/lib.rs (target/debug/deps/mon_api_maths-da3e7a1947867a35)
running 1 test
test tests::test_limite_debordement ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/mon_api_maths-44a383f15f2f56d8)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests mon_api_maths
running 1 test
test src/lib.rs - calculer_perimetre (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.23s; merged doctests compilation took 0.23s
Nos retrouvons :
- notre nouveau test unitaire :
test tests::test_limite_debordement ... oket - le test de l’exemple de documentation :
test src/lib.rs - calculer_perimetre (line 5) ... ok