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 cibles, testant un module à la fois de maniere isolee, et peuvent tester les interfaces privees. Les tests d’integration sont entierement 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 exercant 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 a 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 economise du temps de compilation quand vous voulez seulement compiler la bibliothèque et economise de l’espace dans l’artefact compilé resultant parce que les tests ne sont pas inclus. Vous verrez que comme les tests d’intégration sont dans un repertoire 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 rust,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-01/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 à un debat au sein de la communauté des testeurs pour savoir si les fonctions privees doivent être testees directement ou non, et d’autres langages rendent difficile ou impossible le test des fonctions privees. Quelle que soit l’ideologie de test a laquelle vous adherez, les règles de confidentialite de Rust vous permettent de tester les fonctions privees. Considérez le code de l’encart 11-12 avec la fonction privee 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 marquee 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”][paths], les éléments des modules enfants peuvent utiliser les éléments de leurs modules ancetres. 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 privees ne devraient pas être testees, 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 un crate individuel.

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 un crate séparé, 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 repertoire tests de maniere speciale et ne compilé les fichiers de ce repertoire que lorsque nous exécutons cargo test. Exécutez cargo test maintenant : console {{#include ../listings/ch11-writing-automated-tests/listing-11-13/output.txt}}

$ 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 nomme internal que nous avons ajouté dans l’encart 11-12) puis une ligne de resume pour les tests unitaires.

La section des tests d’intégration commence par la ligne Running tests/integration_test.rs. Ensuite, il y à une ligne pour chaque fonction de test dans ce test d’intégration et une ligne de resume 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 particuliere en specifiant 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 : console {{#include ../listings/ch11-writing-automated-tests/output-only-05-single-integration/output.txt}}

$ 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 repertoire 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”][separating-modules-into-files] du chapitre 7 pour les extraire dans un module commun. Par exemple, si nous créons tests/common.rs et y placons une fonction nommee setup, nous pouvons ajouter du code a setup que nous voulons appeler depuis plusieurs fonctions de test dans plusieurs fichiers de test :

Fichier : tests/common.rs rust,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-12-shared-test-code-problem/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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-12-shared-test-code-problem/output.txt}}

$ 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 affiche 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 repertoire du projet ressemble maintenant a 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”][alt-paths] au chapitre 7. Nommer le fichier de cette façon indique a Rust de ne pas traiter le module common comme un fichier de test d’intégration. Quand nous deplacons 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-repertoires du repertoire 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 rust,ignore {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-13-fix-shared-test-code-problem/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 un 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 repertoire tests et importer les fonctions définies dans le fichier src/main.rs dans la portée avec une instruction use. Seuls les crates de bibliothèque exposent des fonctions que d’autres crates peuvent utiliser ; les crates binaires sont destines a être exécutés de maniere 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 reside dans le fichier src/lib.rs. Avec cette structure, les tests d’intégration peuvent tester le crate de bibliothèque avec use pour rendre la fonctionnalité importante disponible. Si la fonctionnalité importante fonctionne, la petite quantite de code dans le fichier src/main.rs fonctionnera également, et cette petite quantite de code n’a pas besoin d’être testee.

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 a 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 prives. 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 maniere que le code externe l’utilisera. Même si le système de types et les règles de possession de Rust aident a prevenir certains types de bogues, les tests restent importants pour reduire les bogues logiques lies à la façon dont votre code est cense se comporter.

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