Laboratoire du Livre Rust : Tests unitaires et d'intégration

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Bienvenue dans Test Organization. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons apprendre les deux principales catégories de tests dans la communauté Rust : les tests unitaires, qui sont petits et axés sur le test d'un module individuel isolément, et les tests d'intégration, qui utilisent l'interface publique de la bibliothèque et peuvent potentiellement tester plusieurs modules par test.

Test Organization

Comme mentionné au début du chapitre, le test est une discipline complexe et différents personnes utilisent des terminologies et des organisations différentes. La communauté Rust considère les tests en termes de deux principales catégories : les tests unitaires et les tests d'intégration. Les tests unitaires sont petits et plus axés, testant un module isolément à la fois 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 manière que n'importe quel autre code externe, en utilisant seulement l'interface publique et en testant potentiellement plusieurs modules par test.

Ecrire les deux types de tests est important pour s'assurer que les parties de votre bibliothèque font ce que vous attendez d'elles, séparément et ensemble.

Tests unitaires

Le but des tests unitaires est de tester chaque unité de code isolément du reste du code pour localiser rapidement les parties du code qui fonctionnent ou ne fonctionnent pas comme prévu. Vous placerez les tests unitaires dans le répertoire src dans chaque fichier contenant le code qu'ils testent. La convention est de créer un module nommé tests dans chaque fichier pour contenir les fonctions de test et d'ajouter l'annotation cfg(test) au module.

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 seulement lorsque vous exécutez cargo test, et non lorsque vous exécutez cargo build. Cela économise le temps de compilation lorsque vous ne voulez que construire la bibliothèque et économise de l'espace dans l'artefact compilé résultant car les tests ne sont pas inclus. Vous verrez que puisque les tests d'intégration sont dans un répertoire différent, ils n'ont pas besoin de l'annotation #[cfg(test)]. Cependant, puisque les tests unitaires sont dans les mêmes fichiers que le code, vous utiliserez #[cfg(test)] pour spécifier qu'ils ne devraient pas être inclus dans le résultat compilé.

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

Nom de fichier : src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

Ce code est le module tests généré automatiquement. L'attribut cfg signifie configuration et indique à Rust que l'élément suivant ne devrait être inclus que si une certaine option de configuration est donnée. Dans ce cas, l'option de configuration est test, qui est fournie par Rust pour compiler et exécuter les tests. En utilisant l'attribut cfg, Cargo compile notre code de test seulement si nous exécutons activement les tests avec cargo test. Cela inclut toutes les fonctions d'aide qui pourraient être dans ce module, en plus des fonctions annotées avec #[test].

Test des fonctions privées

Il existe un débat dans la communauté des tests sur le fait que les fonctions privées devraient ou non être testées directement, et d'autres langages rendent difficile voire impossible de tester les fonctions privées. Indépendamment de l'idéologie de test à laquelle vous adhérez, les règles de confidentialité de Rust vous permettent effectivement de tester les fonctions privées. Considérez le code de la Liste 11-12 avec la fonction privée internal_adder.

Nom de fichier : src/lib.rs

pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

Liste 11-12 : Test d'une fonction privée

Remarquez que la fonction internal_adder n'est pas marquée pub. Les tests sont simplement du code Rust, et le module tests n'est qu'un autre module. Comme nous l'avons discuté dans "Chemins pour faire référence à un élément dans l'arborescence de modules", les éléments dans les modules enfants peuvent utiliser les éléments de leurs modules ancêtres. Dans ce test, nous mettons tous les éléments du parent du module test dans la portée avec use super::*, et ensuite le test peut appeler internal_adder. Si vous ne pensez pas que les fonctions privées devraient être testées, rien en Rust ne vous forcera à le faire.

Tests d'intégration

En Rust, les tests d'intégration sont entièrement externes à votre bibliothèque. Ils utilisent 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'API publique de votre bibliothèque. Leur but est de tester si de nombreuses parties de votre bibliothèque fonctionnent correctement ensemble. Des unités de code qui fonctionnent correctement individuellement peuvent présenter des problèmes lorsqu'elles sont intégrées, il est donc également important de couvrir le code intégré par des tests. 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 supérieur de notre répertoire de projet, à côté de src. Cargo sait chercher les fichiers de test d'intégration dans ce répertoire. Nous pouvons ensuite 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 la Liste 11-12 toujours dans le fichier src/lib.rs, créez un répertoire tests, puis un nouveau fichier nommé tests/integration_test.rs. La structure de votre répertoire devrait ressembler à ceci :

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

Entrez le code de la Liste 11-13 dans le fichier tests/integration_test.rs.

Nom de fichier : tests/integration_test.rs

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

Liste 11-13 : Un test d'intégration d'une fonction dans la crate adder

Chaque fichier dans le répertoire tests est une crate séparée, donc nous devons inclure notre bibliothèque dans la portée de chaque crate de test. C'est pourquoi nous ajoutons use adder; en haut du code, ce que nous n'avions pas besoin pour les tests unitaires.

Nous n'avons pas besoin d'ajouter l'annotation #[cfg(test)] à aucun code dans tests/integration_test.rs. 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 maintenant cargo test :

[object Object]

Les trois sections de sortie incluent les tests unitaires, le test d'intégration et les tests de documentation. Notez que si un test dans une section échoue, les sections suivantes ne seront pas exécutées. Par exemple, si un test unitaire échoue, 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 sont passés.

La première section pour les tests unitaires [1] est la même que celle que nous avons vue jusqu'à présent : une ligne pour chaque test unitaire (une nommé internal que nous avons ajouté dans la Liste 11-12) puis une ligne de synthèse pour les tests unitaires.

La section des tests d'intégration commence par la ligne Running tests/integration_test.rs [2]. Ensuite, il y a une ligne pour chaque fonction de test dans ce test d'intégration [3] et une ligne de synthèse pour les résultats du test d'intégration [4] juste avant le début de la section Doc-tests adder.

Chaque fichier de test d'intégration a sa propre section, donc si nous ajoutons plus de fichiers dans le répertoire tests, il y aura plus de sections de test 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 en tant qu'argument de cargo test. Pour exécuter tous les tests dans un fichier de test d'intégration particulier, utilisez l'argument --test de cargo test suivi du nom du fichier :

[object Object]

Cette commande exécute seulement les tests dans le fichier tests/integration_test.rs.

Les sous-modules dans les tests d'intégration

Au fur et à mesure que vous ajoutez plus de tests d'intégration, vous pouvez vouloir créer plus de fichiers dans le répertoire tests pour mieux les organiser ; par exemple, vous pouvez regrouper les fonctions de test en fonction de la fonctionnalité qu'elles testent. Comme mentionné précédemment, chaque fichier dans le répertoire tests est compilé comme une crate séparée, ce qui est utile pour créer des portées séparées pour imiter plus étroitement la manière dont les utilisateurs finaux utiliseront votre crate. Cependant, cela signifie que les fichiers dans le répertoire tests n'ont pas le même comportement que les fichiers dans src, comme vous l'avez appris au chapitre 7 sur la manière de séparer le code en modules et en fichiers.

Le comportement différent des fichiers du répertoire tests est le plus évident lorsque vous avez un ensemble de fonctions d'aide à utiliser dans plusieurs fichiers de test d'intégration et que vous essayez de suivre les étapes de "Séparation des modules dans différents fichiers" 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 souhaitons appeler à partir de plusieurs fonctions de test dans plusieurs fichiers de test :

Nom de fichier : tests/common.rs

pub fn setup() {
    // Le code de configuration spécifique aux tests de votre bibliothèque irait ici
}

Lorsque 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 pas appelé la fonction setup depuis nulle part :

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

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

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

C'est la convention de nommage plus ancienne que Rust comprend également que nous avons mentionnée dans "Chemins de fichiers alternatifs". Nommer le fichier de cette manière indique à Rust de ne pas traiter le module common comme un fichier de test d'intégration. Lorsque nous déplaçons le code de la fonction setup dans tests/common/mod.rs et que nous supprimons le fichier tests/common.rs, la section dans la sortie des tests ne s'affichera plus. Les fichiers dans les sous-répertoires du répertoire tests ne sont pas compilés comme des crates séparées ni n'ont de sections dans la sortie des tests.

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

Nom de fichier : tests/integration_test.rs

use adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

Notez que la déclaration mod common; est la même que la déclaration de module que nous avons démontrée dans la Liste 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 que le 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 porter les fonctions définies dans le fichier src/main.rs dans la portée avec une instruction use. Seules les crates bibliothèques exposent des fonctions que d'autres crates peuvent utiliser ; les crates binaires sont destinées à être exécutées seule.

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

Sommaire

Félicitations ! Vous avez terminé le laboratoire d'Organisation des tests. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.