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 attendiez 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
dossier 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 de tests et #[cfg(test)]
L'annotation #[cfg(test)]
sur les modules de 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'artefact 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
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let resultat = 2 + 2;
assert_eq!(resultat, 4);
}
}
Ce code est le module de test 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]
.
Tester des 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, voir 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
.
Fichier : src/lib.rs
pub fn ajouter_deux(a: i32) -> i32 {
addition_interne(a, 2)
}
fn addition_interne(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interne() {
assert_eq!(4, addition_interne(2, 2));
}
}
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
du 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 dossier tests.
Le dossier tests
Nous créons un dossier tests au niveau le plus haut de notre dossier projet, juste à côté de src. Cargo sait qu'il doit rechercher les fichiers de test d'intégration dans ce dossier. Nous pouvons ensuite construire autant de fichiers de test que nous le souhaitons dans ce dossier, 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 dossier tests, puis un nouveau fichier tests/test_integration.rs et insérez-y le code de l'encart 11-13.
Fichier : tests/test_integration.rs
use addition;
#[test]
fn cela_ajoute_deux() {
assert_eq!(4, addition::ajouter_deux(2));
}
Nous avons ajouté use addition
en haut du code, ce que nous n'avions pas
besoin de faire dans les tests unitaires. La raison à cela est que chaque
fichier dans le dossier tests
est une crate séparée, donc nous devons importer
notre bibliothèque dans la portée de chaque crate de test.
Nous n'avons pas besoin de marquer du code avec #[cfg(test)]
dans
tests/test_integration.rs. Cargo traite le dossier tests
de manière
particulière et compile les fichiers présents dans ce dossier 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 [unoptimized + debuginfo] target(s) in 1.31s
Running unittests (target/debug/deps/addition-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. 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 target/debug/deps/test_integration-1082c4b063a8fbe6
(le hachage à la fin de
votre sortie pourrait être différent). 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.
De la même façon que plus vous ajoutiez de fonctions de tests unitaires et plus vous aviez de lignes de résultats dans la section des tests unitaires, plus vous ajoutez des fonctions de tests aux fichiers de tests d'intégration et plus vous obtenez de lignes de résultat dans la section correspondant aux fichiers des tests d'intégration. Chaque fichier de test d'intégration a sa propre section, donc si nous ajoutons plus de fichiers dans le dossier tests, il y aura plus de sections de tests d'intégration.
Nous pouvons aussi 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 [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 dossier tests pour vous aider à les organiser ; par exemple, vous pouvez regrouper les fonctions de test par fonctionnalités qu'elles testent. Comme mentionné précédemment, chaque fichier dans le dossier tests est compilé comme étant sa propre crate séparée de tous les autres.
Le fait que chaque fichier de test d'intégration soit sa propre crate est utile pour créer des portées séparées qui ressemblent à la manière dont les développeurs vont consommer votre crate. Cependant, cela veut aussi dire que les fichiers dans le dossier tests ne partagent pas le même comportement que les 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.
Ce comportement différent des fichiers dans le dossier tests est encore plus
notable lorsque vous avez un jeu de fonctions d'aide qui s'avèrent utiles
pour 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
#![allow(unused)] fn main() { pub fn parametrage() { // 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 [unoptimized + debuginfo] target(s) in 0.89s
Running unittests (target/debug/deps/addition-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/common.rs (target/debug/deps/common-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. C'est
une convention de nommage alternative que Rust comprend aussi. 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 dossier 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 :
Fichier : tests/integration_test.rs
use addition;
mod common;
#[test]
fn cela_ajoute_deux() {
common::parametrage();
assert_eq!(4, addition::ajouter_deux(2));
}
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 dossier 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
importer les importantes fonctionnalités disponibles. Si les fonctionnalités
importantes fonctionnent, 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 !