Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

L’organisation des tests

Comme mentionné au début du chapitre, les tests sont une discipline complexe, et différentes personnes utilisent une terminologie et une organisation différentes. La communauté Rust pense aux tests en termes de deux catégories principales : les tests unitaires et les tests d’intégration. Les tests unitaires sont petits et plus ciblés, testant un module à la fois de manière isolée, et peuvent tester les interfaces privées. Les tests d’intégration sont entièrement externes à votre bibliothèque et utilisent votre code de la même façon que n’importe quel autre code externe le ferait, en n’utilisant que l’interface publique et en exerçant potentiellement plusieurs modules par test.

Écrire les deux types de tests est important pour s’assurer que les composants de votre bibliothèque font ce que vous attendez d’eux, séparément et ensemble.

Tests unitaires

L’objectif des tests unitaires est de tester chaque unité de code isolement du reste du code pour identifier rapidement ou le code fonctionne ou ne fonctionne pas comme prevu. Vous placerez les tests unitaires dans le repertoire src dans chaque fichier avec le code qu’ils testent. La convention est de créer un module nomme tests dans chaque fichier pour contenir les fonctions de test et d’annoter le module avec cfg(test).

Le module tests et #[cfg(test)]

L’annotation #[cfg(test)] sur le module tests indique à Rust de compiler et d’exécuter le code de test uniquement quand vous exécutez cargo test, et non quand vous exécutez cargo build. Cela économise du temps de compilation quand vous voulez seulement compiler la bibliothèque et économise de l’espace dans l’artefact compilé résultant parce que les tests ne sont pas inclus. Vous verrez que comme les tests d’intégration sont dans un répertoire différent, ils n’ont pas besoin de l’annotation #[cfg(test)]. Cependant, comme les tests unitaires sont dans les mêmes fichiers que le code, vous utiliserez #[cfg(test)] pour spécifier qu’ils ne doivent pas être inclus dans le résultat compilé.

Rappelez-vous que lorsque nous avons génère le nouveau projet adder dans la première section de ce chapitre, Cargo a génère ce code pour nous :

Fichier : src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Sur le module tests génère automatiquement, l’attribut cfg signifie configuration et indique a Rust que l’élément suivant ne doit être inclus que pour une certaine option de configuration. 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 ne compilé notre code de test que si nous exécutons activement les tests avec cargo test. Cela inclut toutes les fonctions utilitaires qui pourraient se trouver dans ce module, en plus des fonctions annotees avec #[test].

Tests de fonctions privees

Il y a un débat au sein de la communauté des testeurs pour savoir si les fonctions privées doivent être testées directement ou non, et d’autres langages rendent difficile ou impossible le test des fonctions privées. Quelle que soit l’idéologie de test à laquelle vous adhérez, les règles de confidentialité de Rust vous permettent de tester les fonctions privées. Considérez le code de l’encart 11-12 avec la fonction privée internal_adder.

Filename: src/lib.rs
pub fn add_two(a: u64) -> u64 {
    internal_adder(a, 2)
}

fn internal_adder(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}
Listing 11-12: Testing a private function

Notez que la fonction internal_adder n’est pas marquée comme pub. Les tests ne sont que du code Rust, et le module tests n’est qu’un autre module. Comme nous l’avons vu dans “Les chemins pour faire référence à un élément dans l’arborescence des modules”, les éléments des modules enfants peuvent utiliser les éléments de leurs modules ancêtres. Dans ce test, nous importons tous les éléments appartenant au parent du module tests dans la portée avec use super::*, et le test peut alors appeler internal_adder. Si vous pensez que les fonctions privées ne devraient pas être testées, rien dans Rust ne vous obligera à le faire.

Tests d’intégration

En Rust, les tests d’intégration sont entierement externes à votre bibliothèque. Ils utilisent votre bibliothèque de la même façon que n’importe quel autre code le ferait, ce qui signifie qu’ils ne peuvent appeler que les fonctions faisant partie de l’API publique de votre bibliothèque. Leur objectif est de tester si de nombreuses parties de votre bibliothèque fonctionnent correctement ensemble. Des unités de code qui fonctionnent correctement individuellement pourraient avoir des problèmes une fois intégrées, donc la couverture de test du code integre est également importante. Pour créer des tests d’intégration, vous avez d’abord besoin d’un repertoire tests.

Le repertoire tests

Nous créons un repertoire tests au niveau supérieur de notre repertoire de projet, a côté de src. Cargo sait qu’il doit chercher les fichiers de tests d’intégration dans ce repertoire. Nous pouvons alors créer autant de fichiers de test que nous le souhaitons, et Cargo compilera chacun des fichiers comme une crate individuelle.

Créons un test d’intégration. Avec le code de l’encart 11-12 toujours dans le fichier src/lib.rs, créez un repertoire tests et créez un nouveau fichier nomme tests/integration_test.rs. Votre structure de repertoire devrait ressembler a 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.

Filename: tests/integration_test.rs
use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}
Listing 11-13: An integration test of a function in the adder crate

Chaque fichier dans le repertoire tests est une crate séparée, nous devons donc importer notre bibliothèque dans la portée de chaque crate de test. C’est pourquoi nous ajoutons use adder::add_two; en haut du code, ce que nous n’avions pas besoin de faire dans les tests unitaires.

Nous n’avons pas besoin d’annoter le code dans tests/integration_test.rs avec #[cfg(test)]. Cargo traite le répertoire tests de manière spéciale et ne compile les fichiers de ce répertoire que lorsque nous exécutons cargo test. Exécutez cargo test maintenant :

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    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::internal ... 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 it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

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 sortie comprennent les tests unitaires, le test d’intégration et les tests de documentation. Notez que si un test d’une section echoue, les sections suivantes ne seront pas exécutées. Par exemple, si un test unitaire echoue, il n’y aura pas de sortie pour les tests d’intégration et de documentation, car ces tests ne seront exécutés que si tous les tests unitaires reussissent.

La première section pour les tests unitaires est la même que ce que nous avons vu : une ligne pour chaque test unitaire (un nommé internal que nous avons ajouté dans l’encart 11-12) puis une ligne de résumé pour les tests unitaires.

La section des tests d’intégration commence par la ligne Running tests/integration_test.rs. Ensuite, il y a une ligne pour chaque fonction de test dans ce test d’intégration et une ligne de résumé pour les résultats du test d’intégration juste avant le début de la section Doc-tests adder.

Chaque fichier de test d’intégration à sa propre section, donc si nous ajoutons plus de fichiers dans le repertoire tests, il y aura plus de sections de tests d’intégration.

Nous pouvons toujours exécuter une fonction de test d’intégration particulière en spécifiant le nom de la fonction de test comme argument de cargo test. Pour exécuter tous les tests d’un fichier de test d’intégration particulier, utilisez l’argument --test de cargo test suivi du nom du fichier :

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    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 it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cette commande n’exécute que les tests du fichier tests/integration_test.rs.

Sous-modules dans les tests d’intégration

Au fur et a mesure que vous ajoutez des tests d’intégration, vous pourriez vouloir créer plus de fichiers dans le repertoire tests pour les organiser ; par exemple, vous pouvez regrouper les fonctions de test par la fonctionnalité qu’elles testent. Comme mentionné precedemment, chaque fichier dans le repertoire tests est compilé comme son propre crate séparé, ce qui est utile pour créer des portées séparées afin d’imiter plus fidèlement la façon dont les utilisateurs finaux utiliseront votre crate. Cependant, cela signifie que les fichiers dans le repertoire tests ne partagent pas le même comportement que les fichiers dans src, comme vous l’avez appris au chapitre 7 concernant la séparation du code en modules et fichiers.

Le comportement différent des fichiers du répertoire tests est plus visible quand vous avez un ensemble de fonctions utilitaires à utiliser dans plusieurs fichiers de tests 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 pour les extraire dans un module commun. Par exemple, si nous créons tests/common.rs et y plaçons une fonction nommée setup, nous pouvons ajouter du code à setup que nous voulons appeler depuis plusieurs fonctions de test dans plusieurs fichiers de test :

Fichier : tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

Quand nous exécutons les tests à nouveau, nous verrons une nouvelle section dans la sortie des tests pour le fichier common.rs, même si ce fichier ne contient aucune fonction de test et que nous n’avons appelé la fonction setup depuis nulle part :

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    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::internal ... 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 it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Voir common apparaître dans les résultats de test avec running 0 tests affiché n’est pas ce que nous voulions. Nous voulions simplement partager du code avec les autres fichiers de tests d’intégration. Pour éviter que common apparaisse dans la sortie des tests, au lieu de créer tests/common.rs, nous allons créer tests/common/mod.rs. Le répertoire du projet ressemble maintenant à ceci :

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

C’est l’ancienne convention de nommage que Rust comprend également et que nous avons mentionnée dans “Chemins de fichiers alternatifs” au chapitre 7. Nommer le fichier de cette façon indique à Rust de ne pas traiter le module common comme un fichier de test d’intégration. Quand nous déplaçons le code de la fonction setup dans tests/common/mod.rs et supprimons le fichier tests/common.rs, la section dans la sortie des tests n’apparaîtra plus. Les fichiers dans les sous-répertoires du répertoire tests ne sont pas compilés comme des crates séparés et n’ont pas de sections dans la sortie des tests.

Après avoir crée tests/common/mod.rs, nous pouvons l’utiliser depuis n’importe quel fichier de test d’intégration en tant que module. Voici un exemple d’appel de la fonction setup depuis le test it_adds_two dans tests/integration_test.rs :

Fichier : tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

Notez que la déclaration mod common; est la même que la déclaration de module que nous avons montrée dans l’encart 7-21. Ensuite, dans la fonction de test, nous pouvons appeler la fonction common::setup().

Tests d’intégration pour les crates binaires

Si notre projet est une crate binaire qui ne contient qu’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 la portée avec une instruction use. Seules les crates de bibliothèque exposent des fonctions que d’autres crates peuvent utiliser ; les crates binaires sont destinées à être exécutées de manière autonome.

C’est l’une des raisons pour lesquelles les projets Rust qui fournissent un binaire ont un fichier src/main.rs simple qui appelle la logique qui réside dans le fichier src/lib.rs. Avec cette structure, les tests d’intégration peuvent tester la crate de bibliothèque avec use pour rendre la fonctionnalité importante disponible. Si la fonctionnalité importante fonctionne, la petite quantité de code dans le fichier src/main.rs fonctionnera également, et cette petite quantité de code n’a pas besoin d’être testée.

Résumé

Les fonctionnalités de test de Rust fournissent un moyen de spécifier comment le code devrait fonctionner pour s’assurer qu’il continue à fonctionner comme vous l’attendez, même lorsque vous apportez des modifications. Les tests unitaires exercent différentes parties d’une bibliothèque séparément et peuvent tester les détails d’implémentation 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 code externe l’utilisera. Même si le système de types et les règles de possession de Rust aident à prévenir certains types de bogues, les tests restent importants pour réduire les bogues logiques liés à la façon dont votre code est censé se comporter.

Combinons les connaissances que vous avez acquises dans ce chapitre et dans les chapitres précédents pour travailler sur un projet !