L’organisation des tests
Comme nous l’avons évoqué au début du chapitre, le test est une discipline complexe, et différentes personnes utilisent des terminologies et organisations différentes. La communauté Rust a conçu les tests dans deux catégories principales : les tests unitaires et les tests d’intégration. Les tests unitaires sont petits et plus précis, testent un module isolé à la fois, et peuvent tester les interfaces privées. Les tests d’intégration sont uniquement externes à notre bibliothèque et consomment notre code exactement de la même manière que tout autre code externe le ferait, en utilisant uniquement l’interface publique et éventuellement en utilisant plusieurs modules dans un test.
L’écriture de ces deux types de tests est importante pour s’assurer que chaque élément de notre bibliothèque fait bien ce que vous attendez d’eux, de manière isolée et conjuguée avec d’autres.
Les tests unitaires
Le but des tests unitaires est de tester chaque élément du code de manière séparée du reste du code pour identifier rapidement où le code fonctionne ou non comme prévu. Vous devriez insérer les tests unitaires dans le répertoire src dans chaque fichier, à côté du code qu’ils testent. La convention est de créer un module tests dans chaque fichier qui contient les fonctions de test et de marquer le module avec cfg(test).
Les modules tests et #[cfg(test)]
L’annotation #[cfg(test)] sur les modules tests indique à Rust de compiler et d’exécuter le code de test seulement lorsque vous lancez cargo test, et non pas lorsque vous lancez cargo build. Cela diminue la durée de compilation lorsque vous souhaitez uniquement compiler la bibliothèque et cela réduit la taille dans l’artéfact compilé qui en résulte car les tests n’y sont pas intégrés. Vous verrez plus tard que comme les tests d’intégration se placent dans un répertoire différent, ils n’ont pas besoin de l’annotation #[cfg(test)]. Cependant, comme les tests unitaires vont dans les mêmes fichiers que le code, vous devriez utiliser #[cfg(test)] pour marquer qu’ils ne devraient pas être inclus dans les résultats de compilation.
Souvenez-vous, lorsque nous avons généré le nouveau projet addition dans la première section de ce chapitre, Cargo a généré ce code pour nous :
Fichier : src/lib.rs
pub fn additionne(gauche: u64, droite: u64) -> u64 {
gauche + droite
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cela_fonctionne() {
let resultat = additionne(2, 2);
assert_eq!(resultat, 4);
}
}
Dans le module tests généré automatiquement, l’attribut cfg est l’abréviation de configuration et indique à Rust que l’élément suivant ne doit être intégré que lorsqu’une certaine option de configuration est donnée. Dans ce cas, l’option de configuration est test, qui est fournie par Rust pour la compilation et l’exécution des tests. En utilisant l’attribut cfg, Cargo compile notre code de tests uniquement si nous avons exécuté les tests avec cargo test. Cela inclut toutes les fonctions auxiliaires qui pourraient se trouver dans ce module, en plus des fonctions marquées d’un #[test].
Tests de fonctions privées
Il existe un débat dans la communauté des testeurs au sujet de la nécessité ou non de tester directement les fonctions privées, et d’autres langages rendent difficile, voire impossible, de tester les fonctions privées. Quelle que soit votre approche des tests, les règles de protection de Rust vous permettent de tester des fonctions privées. Imaginons le code de l’encart 11-12 qui contient la fonction privée addition_interne.
pub fn ajouter_deux(a: u64) -> u64 {
addition_interne(a, 2)
}
fn addition_interne(gauche: u64, droite: u64) -> u64 {
gauche + droite
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interne() {
let resultat = addition_interne(2, 2);
assert_eq!(resultat, 4);
}
}
Remarquez que la fonction addition_interne n’est pas marquée comme pub. Les tests sont uniquement du code Rust, et le module test est simplement un autre module. Comme nous l’avons vu dans la section “Désigner un élément dans l’arborescence de modules”, les éléments dans les modules enfants peuvent utiliser les éléments dans leurs modules parents. Dans ce test, nous importons dans la portée tous les éléments appartenant au parent du module test grâce à use super::*;, permettant ensuite au test de faire appel à addition_interne. Si vous pensez qu’une fonction privée ne doit pas être testée, il n’y a rien qui vous y force avec Rust.
Les tests d’intégration
En Rust, les tests d’intégration sont exclusivement externes à votre bibliothèque. Ils consomment votre bibliothèque de la même manière que n’importe quel autre code, ce qui signifie qu’ils ne peuvent appeler que les fonctions qui font partie de l’interface de programmation applicative (API) publique de votre bibliothèque. Leur but est de tester si les multiples parties de votre bibliothèque fonctionnent correctement ensemble. Les portions de code qui fonctionnent bien toutes seules pourraient rencontrer des problèmes une fois imbriquées avec d’autres, donc les tests qui couvrent l’intégration du code sont tout aussi importants. Pour créer des tests d’intégration, vous avez d’abord besoin d’un répertoire tests.
Le répertoire tests
Nous créons un répertoire tests au niveau le plus haut de notre répertoire projet, juste à côté de src. Cargo sait qu’il doit rechercher les fichiers de test d’intégration dans ce répertoire. Nous pouvons ensuite y construire autant de fichiers de test que nous le souhaitons, et Cargo va compiler chacun de ces fichiers comme une crate individuelle.
Commençons à créer un test d’intégration. Avec le code de l’encart 11-12 toujours présent dans le fichier src/lib.rs, créez un répertoire tests, puis un nouveau fichier tests/test_integration.rs et insérez-y le code de l’encart 11-13. Votre structure de répertoires doit ressembler à ceci :
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
Entrez le code de l’encart 11-13 dans le fichier tests/integration_test.rs.
use addition::ajouter_deux;
#[test]
fn cela_ajoute_deux() {
let resultat = ajouter_deux(2);
assert_eq!(resultat, 4);
}
adder crateChaque fichier dans le répertoire tests est une crate séparée, donc nous devons importer notre bibliothèque dans la portée de chaque crate de test. Pour cette raison, nous avons ajouté use addition en haut du code, ce que nous n’avions pas besoin de faire dans les tests unitaires.
Nous n’avons pas besoin de marquer du code avec #[cfg(test)] dans tests/test_integration.rs. Cargo traite le répertoire tests de manière particulière et compile les fichiers présents dans ce répertoire uniquement si nous lançons cargo test. Lancez dès maintenant cargo test :
$ cargo test
Compiling addition v0.1.0 (file:///projects/addition)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
running 1 test
test tests::interne ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
running 1 test
test cela_ajoute_deux ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests addition
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Les trois sections de la sortie concernent les tests unitaires, les tests d’intégration et les tests de documentation. Notez que si le moindre test d’une section échoue, les sections suivantes ne seront pas exécutées. Par exemple, si un test unitaire échoue, il n’y aura aucun résultat pour les tests d’intégration et les tests de documentation, car ces tests ne sont exécutés que si tous les tests unitaires ont réussi.
La première section relative aux tests unitaires est la même que celle que nous avons déjà vue : une ligne pour chaque test unitaire (celui qui s’appelle interne que nous avons inséré dans l’encart 11-12) suivie d’une ligne de résumé des tests unitaires.
La section des tests d’intégration commence avec la ligne Running tests/integration_test.rs. Ensuite, il y a une ligne pour chaque fonction de test présente dans ce test d’intégration et une ligne de résumé pour les résultats des tests d’intégration, juste avant que la section Doc-tests addition ne commence.
Chaque test d’intégration a sa propre section ; ainsi si nous ajoutons d’autres fichiers dans le répertoire tests, il y aura plus de sections de tests d’intégration.
Nous pouvons toujours exécuter une fonction de test d’intégration précise en utilisant le nom de la fonction de test comme argument à cargo test. Pour exécuter tous les tests d’un fichier de tests d’intégration précis, utilisez l’argument --test de cargo test suivi du nom du fichier :
$ cargo test --test integration_test
Compiling addition v0.1.0 (file:///projects/addition)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test cela_ajoute_deux ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Cette commande exécute seulement les tests dans le fichier tests/test_integration.rs.
Les sous-modules des tests d’intégration
Au fur et à mesure que vous ajouterez des tests d’intégration, vous pourriez avoir besoin de les diviser en plusieurs fichiers dans le répertoire tests pour vous aider à les organiser. Comme mentionné précédemment, chaque fichier dans le répertoire tests est compilé comme étant sa propre crate séparée de toutes les autres, ce qui est utile pour créer des portées séparées qui imitent mieux la manière dont les développeurs vont utiliser votre crate. Cependant, cela veut aussi dire que les fichiers dans le répertoire tests ne partagent pas le même comportement que les fichiers dans src, comme vous l’avez appris au chapitre 7 à propos de la manière de séparer le code dans des modules et des fichiers.
Le comportement différent des fichiers dans le répertoire tests est encore plus notable lorsque vous avez un jeu de fonctions d’aide à utiliser dans plusieurs fichiers de test d’intégration et que vous essayez de suivre les étapes de la section “Séparer les modules dans différents fichiers” du chapitre 7 afin de les extraire dans un module en commun. Par exemple, si nous créons tests/commun.rs et que nous y plaçons une fonction parametrage à l’intérieur, nous pourrions ajouter du code à parametrage que nous voudrions appeler à partir de différentes fonctions de test dans différents fichiers de test :
Fichier : tests/commun.rs
pub fn setup() {
// code de paramétrage spécifique à vos tests de votre bibliothèque ici
}
Lorsque nous lançons les tests à nouveau, nous allons voir une nouvelle section dans la sortie des tests, correspondant au fichier commun.rs, même si ce fichier ne contient aucune fonction de test et que nous n’avons utilisé nulle part la fonction parametrage :
$ cargo test
Compiling addition v0.1.0 (file:///projects/addition)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::interne ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/commun.rs (target/debug/deps/commun-92948b65e88960b4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
running 1 test
test cela_ajoute_deux ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests addition
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Nous ne voulons pas que commun apparaisse dans les résultats, ni que cela affiche running 0 tests. Nous voulons juste partager du code avec les autres fichiers de test d’intégration. Pour éviter que commun s’affiche sur la sortie de test, au lieu de créer le fichier tests/commun.rs, nous allons créer tests/commun/mod.rs. Le répertoire du projet ressemble maintenant à ceci :
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── commun
│ └── mod.rs
└── integration_test.rs
C’est là l’ancienne convention de nommage que Rust comprend, que nous avons mentionnée dans “Séparer les modules dans différents fichiers” au chapitre 7. Nommer le fichier ainsi indique à Rust de ne pas traiter le module commun comme un fichier de test d’intégration. Lorsque nous déplaçons le code de la fonction parametrage dans tests/commun/mod.rs et que nous supprimons le fichier tests/commun.rs, la section dans la sortie des tests ne va plus s’afficher. Les fichiers dans les sous-répertoires du répertoire tests ne seront pas compilés comme étant une crate séparée et n’auront pas de sections dans la sortie des tests.
Après avoir créé tests/commun/mod.rs, nous pouvons l’utiliser à partir de n’importe quel fichier de test d’intégration comme un module. Voici un exemple d’appel à la fonction parametrage à partir du test cela_ajoute_deux dans tests/test_integration.rs :
Filename: tests/integration_test.rs
use addition::ajouter_deux;
mod commun;
#[test]
fn cela_ajoute_deux() {
commun::parametrage();
let resultat = ajouter_deux(2);
assert_eq!(resultat, 4);
}
Remarquez que la déclaration mod commun; est la même que la déclaration d’un module que nous avons montrée dans l’encart 7-21. Ensuite, dans la fonction de tests, nous pouvons appeler la fonction commun::parametrage.
Tests d’intégration pour les crates binaires
Si notre projet est une crate binaire qui contient uniquement un fichier src/main.rs et n’a pas de fichier src/lib.rs, nous ne pouvons pas créer de tests d’intégration dans le répertoire tests et importer les fonctions définies dans le fichier src/main.rs dans notre portée avec une instruction use. Seules les crates de bibliothèque exposent des fonctions que les autres crates peuvent utiliser ; les crates binaires sont conçues pour être exécutées de manière isolée.
C’est une des raisons pour lesquelles les projets Rust qui fournissent un binaire ont un simple fichier src/main.rs qui fait appel à la logique présente dans le fichier src/lib.rs. En utilisant cette structure, les tests d’intégration peuvent tester la crate de bibliothèque avec le use pour pouvoir accéder à la fonctionnalité importante. Si la fonctionnalité importante fonctionne, la petite portion de code dans le fichier src/main.rs va fonctionner, et cette petite partie de code n’a pas besoin d’être testée.
Résumé
Les fonctionnalités de test de Rust permettent de spécifier comment le code doit fonctionner pour garantir qu’il va continuer à fonctionner comme vous le souhaitez, même si vous faites des changements. Les tests unitaires permettent de tester séparément différentes parties d’une bibliothèque et peuvent tester l’implémentation des éléments privés. Les tests d’intégration vérifient que de nombreuses parties de la bibliothèque fonctionnent correctement ensemble, et ils utilisent l’API publique de la bibliothèque pour tester le code, de la même manière que le ferait du code externe qui l’utiliserait. Même si le système de type de Rust et les règles de possession aident à empêcher certains types de bogues, les tests restent toujours importants pour réduire les bogues de logique concernant le comportement attendu de votre code.
Et maintenant, combinons le savoir que vous avez accumulé dans ce chapitre et dans les chapitres précédents en travaillant sur un nouveau projet !