Comment écrire des tests

RustRustBeginner
Pratiquer maintenant

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

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans How to Write Tests. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons apprendre à écrire des tests en Rust en utilisant des attributs, des macros et des assertions.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/ErrorHandlingandDebuggingGroup(["Error Handling and Debugging"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/ErrorHandlingandDebuggingGroup -.-> rust/panic_usage("panic! Usage") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100415{{"Comment écrire des tests"}} rust/integer_types -.-> lab-100415{{"Comment écrire des tests"}} rust/string_type -.-> lab-100415{{"Comment écrire des tests"}} rust/function_syntax -.-> lab-100415{{"Comment écrire des tests"}} rust/expressions_statements -.-> lab-100415{{"Comment écrire des tests"}} rust/method_syntax -.-> lab-100415{{"Comment écrire des tests"}} rust/panic_usage -.-> lab-100415{{"Comment écrire des tests"}} rust/traits -.-> lab-100415{{"Comment écrire des tests"}} end

Comment écrire des tests

Les tests sont des fonctions Rust qui vérifient que le code non de test fonctionne comme prévu. Les corps des fonctions de test effectuent généralement ces trois actions :

  • Préparer toute les données ou l'état nécessaires.
  • Exécuter le code que vous voulez tester.
  • Vérifier que les résultats sont ceux que vous attendez.

Examnons les fonctionnalités que Rust offre spécifiquement pour écrire des tests qui effectuent ces actions, qui incluent l'attribut test, quelques macros et l'attribut should_panic.

L'anatomie d'une fonction de test

Au minimum, un test en Rust est une fonction annotée avec l'attribut test. Les attributs sont des métadonnées sur des parties de code Rust ; un exemple est l'attribut derive que nous avons utilisé avec les structs au chapitre 5. Pour transformer une fonction en fonction de test, ajoutez #[test] sur la ligne avant fn. Lorsque vous exécutez vos tests avec la commande cargo test, Rust construit un binaire exécuteur de tests qui exécute les fonctions annotées et informe si chaque fonction de test passe ou échoue.

Chaque fois que nous créons un nouveau projet de bibliothèque avec Cargo, un module de test contenant une fonction de test est automatiquement généré pour nous. Ce module vous donne un modèle pour écrire vos tests, de sorte que vous n'ayez pas besoin de chercher la structure et la syntaxe exactes chaque fois que vous commencez un nouveau projet. Vous pouvez ajouter autant de fonctions de test supplémentaires et de modules de test que vous le souhaitez!

Nous allons explorer certains aspects de fonctionnement des tests en expérimentant le test modèle avant de tester réellement du code. Ensuite, nous écrirons quelques tests du monde réel qui appellent du code que nous avons écrit et affirmons que son comportement est correct.

Créons un nouveau projet de bibliothèque appelé adder qui additionnera deux nombres :

$ cargo new adder --lib
Créé le projet de bibliothèque $(adder)
$ cd adder

Le contenu du fichier src/lib.rs dans votre bibliothèque adder devrait ressembler à la liste 11-1.

Nom de fichier : src/lib.rs

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

Liste 11-1 : Le module de test et la fonction générés automatiquement par cargo new

Pour l'instant, ignorons les deux premières lignes et concentrons-nous sur la fonction. Notez l'annotation #[test] [1] : cet attribut indique que cette fonction est une fonction de test, de sorte que l'exécuteur de tests sait traiter cette fonction comme un test. Nous pourrions également avoir des fonctions non de test dans le module tests pour aider à préparer des scénarios communs ou à effectuer des opérations communes, de sorte que nous devons toujours indiquer quelles fonctions sont des tests.

Le corps d'exemple de fonction utilise la macro assert_eq! [2] pour affirmer que result, qui contient le résultat de l'addition de 2 et 2, est égal à 4. Cette assertion sert d'exemple du format d'un test typique. Exécutons-la pour voir que ce test passe.

La commande cargo test exécute tous les tests de notre projet, comme indiqué dans la liste 11-2.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [non optimisé + débogage] cibles(s) en 0,57 s
     Exécution des tests unitaires src/lib.rs (cible/debug/deps/adder-
92948b65e88960b4)

1 exécution d'un test
2 test tests::it_works... ok

3 Résultat du test : ok. 1 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

  4 Tests de documentation adder

exécution de 0 tests

résultat du test : ok. 0 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Liste 11-2 : La sortie de l'exécution du test généré automatiquement

Cargo a compilé et exécuté le test. Nous voyons la ligne exécution d'un test [1]. La ligne suivante montre le nom de la fonction de test générée, appelée it_works, et que le résultat de l'exécution de ce test est ok [2]. Le résumé global résultat du test : ok. [3] signifie que tous les tests ont réussi, et la partie qui lit 1 passé ; 0 échoué totalise le nombre de tests qui ont réussi ou échoué.

Il est possible de marquer un test comme ignoré de sorte qu'il ne s'exécute pas dans une instance particulière ; nous aborderons cela dans "Ignorer certains tests sauf si spécifiquement demandé". Comme nous n'avons pas fait cela ici, le résumé indique 0 ignoré. Nous pouvons également passer un argument à la commande cargo test pour exécuter seulement les tests dont le nom correspond à une chaîne de caractères ; cela s'appelle le filtrage et nous aborderons cela dans "Exécuter un sous-ensemble de tests par nom". Ici, nous n'avons pas filtré les tests en cours d'exécution, de sorte que la fin du résumé indique 0 filtré.

La statistique 0 mesuré est pour les tests de benchmark qui mesurent les performances. Les tests de benchmark sont, à l'heure actuelle, uniquement disponibles dans la version nocturne de Rust. Consultez la documentation sur les tests de benchmark à https://doc.rust-lang.org/unstable-book/library-features/test.html pour en savoir plus.

La partie suivante de la sortie de test commençant par Tests de documentation adder [4] est pour les résultats de tous les tests de documentation. Nous n'avons pas encore de tests de documentation, mais Rust peut compiler tous les exemples de code qui apparaissent dans notre documentation API. Cette fonction aide à maintenir vos documents et votre code synchronisés! Nous discuterons de la manière d'écrire des tests de documentation dans "Commentaires de documentation en tant que tests". Pour l'instant, nous ignorerons la sortie Tests de documentation.

Commencons à personnaliser le test selon nos besoins. Premièrement, changeons le nom de la fonction it_works en un nom différent, tel que exploration, comme ceci :

Nom de fichier : src/lib.rs

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

Ensuite, exécutez cargo test à nouveau. La sortie montre maintenant exploration au lieu de it_works :

exécution d'un test
test tests::exploration... ok

résultat du test : ok. 1 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Maintenant, ajoutons un autre test, mais cette fois-ci, nous allons créer un test qui échoue! Les tests échouent lorsqu'une erreur survient dans la fonction de test. Chaque test est exécuté dans un nouveau fil, et lorsque le fil principal constate qu'un fil de test est mort, le test est marqué comme échoué. Au chapitre 9, nous avons parlé de la manière la plus simple de générer une erreur est d'appeler la macro panic!. Entrez le nouveau test sous forme d'une fonction nommée another, de sorte que votre fichier src/lib.rs ressemble à la liste 11-3.

Nom de fichier : src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Faites échouer ce test");
    }
}

Liste 11-3 : Ajout d'un deuxième test qui échouera car nous appelons la macro panic!

Exécutez les tests à nouveau en utilisant cargo test. La sortie devrait ressembler à la liste 11-4, qui montre que notre test exploration a réussi et que another a échoué.

exécution de 2 tests
test tests::exploration... ok
1 test tests::another... FAILED

2 échecs :

---- tests::another sortie standard ----
thread'main' a généré une erreur à 'Faites échouer ce test', src/lib.rs:10:9
note : exécutez avec la variable d'environnement `RUST_BACKTRACE=1` pour afficher
une trace de pile

3 échecs :
    tests::another

4 résultat du test : FAILED. 1 passé ; 1 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

erreur : le test a échoué, pour le relancer passez '--lib'

Liste 11-4 : Résultats des tests lorsqu'un test passe et qu'un test échoue

Au lieu de ok, la ligne test tests::another montre FAILED [1]. Deux nouvelles sections apparaissent entre les résultats individuels et le résumé : la première [2] affiche la raison détaillée de chaque échec de test. Dans ce cas, nous obtenons les détails selon lesquels another a échoué car il a généré une erreur à 'Faites échouer ce test' à la ligne 10 dans le fichier src/lib.rs. La section suivante [3] liste uniquement les noms de tous les tests qui ont échoué, ce qui est utile lorsqu'il y a beaucoup de tests et beaucoup de sortie détaillée d'échec de test. Nous pouvons utiliser le nom d'un test qui a échoué pour exécuter uniquement ce test pour le déboguer plus facilement ; nous parlerons plus de façons d'exécuter des tests dans "Contrôler la manière dont les tests sont exécutés".

La ligne de résumé s'affiche à la fin [4] : globalement, notre résultat de test est FAILED. Nous avons eu un test qui a réussi et un test qui a échoué.

Maintenant que vous avez vu à quoi ressemblent les résultats des tests dans différents scénarios, examinons quelques macros autres que panic! qui sont utiles dans les tests.

Vérifier les résultats avec la macro assert!

La macro assert!, fournie par la bibliothèque standard, est utile lorsque vous voulez vous assurer qu'une certaine condition dans un test évalue à true. Nous donnons à la macro assert! un argument qui évalue à un booléen. Si la valeur est true, rien ne se passe et le test passe. Si la valeur est false, la macro assert! appelle panic! pour provoquer l'échec du test. Utiliser la macro assert! nous aide à vérifier que notre code fonctionne comme nous le souhaitons.

Dans la liste 5-15, nous avons utilisé une struct Rectangle et une méthode can_hold, qui sont répétées ici dans la liste 11-5. Plaçons ce code dans le fichier src/lib.rs, puis écrivons quelques tests pour cela en utilisant la macro assert!.

Nom de fichier : src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Liste 11-5 : Utilisation de la struct Rectangle et de sa méthode can_hold du chapitre 5

La méthode can_hold renvoie un booléen, ce qui signifie qu'il s'agit d'un cas d'utilisation parfait pour la macro assert!. Dans la liste 11-6, nous écrivons un test qui exerce la méthode can_hold en créant une instance de Rectangle qui a une largeur de 8 et une hauteur de 7 et en affirmant qu'elle peut contenir une autre instance de Rectangle qui a une largeur de 5 et une hauteur de 1.

Nom de fichier : src/lib.rs

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

    #[test]
  2 fn larger_can_hold_smaller() {
      3 let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

      4 assert!(larger.can_hold(&smaller));
    }
}

Liste 11-6 : Un test pour can_hold qui vérifie si un rectangle plus grand peut effectivement contenir un rectangle plus petit

Notez que nous avons ajouté une nouvelle ligne dans le module tests : use super::*; [1]. Le module tests est un module normal qui suit les règles de visibilité habituelles que nous avons couvertes dans "Chemins pour faire référence à un élément dans l'arbre de modules". Comme le module tests est un module interne, nous devons amener le code à tester dans le module externe dans la portée du module interne. Nous utilisons un glob ici, de sorte que tout ce que nous définissons dans le module externe est disponible pour ce module tests.

Nous avons nommé notre test larger_can_hold_smaller [2], et nous avons créé les deux instances de Rectangle dont nous avons besoin [3]. Ensuite, nous avons appelé la macro assert! et lui avons passé le résultat de l'appel à larger.can_hold(&smaller) [4]. Cette expression devrait renvoyer true, de sorte que notre test devrait passer. Vérifions!

exécution d'un test
test tests::larger_can_hold_smaller... ok

résultat du test : ok. 1 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Il passe effectivement! Ajoutons un autre test, cette fois en affirmant qu'un rectangle plus petit ne peut pas contenir un rectangle plus grand :

Nom de fichier : src/lib.rs

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

    #[test]
    fn larger_can_hold_smaller() {
        --extrait--
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Comme le résultat correct de la fonction can_hold dans ce cas est false, nous devons négater ce résultat avant de le passer à la macro assert!. En conséquence, notre test passera si can_hold renvoie false :

exécution de 2 tests
test tests::larger_can_hold_smaller... ok
test tests::smaller_cannot_hold_larger... ok

résultat du test : ok. 2 passés ; 0 échoués ; 0 ignorés ; 0 mesurés ; 0
filtrés ; terminé en 0,00 s

Deux tests qui passent! Maintenant, voyons ce qui se passe avec nos résultats de test lorsque nous introduisons une erreur dans notre code. Nous allons modifier l'implémentation de la méthode can_hold en remplaçant le signe supérieur par un signe inférieur lorsqu'elle compare les largeurs :

--extrait--

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

Exécuter les tests produit maintenant ceci :

exécution de 2 tests
test tests::smaller_cannot_hold_larger... ok
test tests::larger_can_hold_smaller... FAILED

échecs :

---- tests::larger_can_hold_smaller sortie standard ----
thread'main' a généré une erreur à 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:28:9
note : exécutez avec la variable d'environnement `RUST_BACKTRACE=1` pour afficher
une trace de pile


échecs :
    tests::larger_can_hold_smaller

résultat du test : FAILED. 1 passé ; 1 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Nos tests ont détecté l'erreur! Comme larger.width est 8 et smaller.width est 5, la comparaison des largeurs dans can_hold renvoie maintenant false : 8 n'est pas inférieur à 5.

Vérifier l'égalité avec les macros assert_eq! et assert_ne!

Un moyen courant de vérifier la fonctionnalité est de tester l'égalité entre le résultat du code à tester et la valeur que vous attendez que le code renvoie. Vous pourriez faire cela en utilisant la macro assert! et en lui passant une expression utilisant l'opérateur ==. Cependant, ce test est si courant que la bibliothèque standard fournit une paire de macros - assert_eq! et assert_ne! - pour effectuer ce test de manière plus pratique. Ces macros comparent deux arguments pour l'égalité ou l'inégalité, respectivement. Elles afficheront également les deux valeurs si l'assertion échoue, ce qui facilite la compréhension pourquoi le test a échoué ; en revanche, la macro assert! indique seulement qu'elle a obtenu une valeur false pour l'expression ==, sans afficher les valeurs qui ont entraîné la valeur false.

Dans la liste 11-7, nous écrivons une fonction nommée add_two qui ajoute 2 à son paramètre, puis nous testons cette fonction en utilisant la macro assert_eq!.

Nom de fichier : src/lib.rs

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

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

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

Liste 11-7 : Test de la fonction add_two en utilisant la macro assert_eq!

Vérifions qu'elle passe!

exécution d'un test
test tests::it_adds_two... ok

résultat du test : ok. 1 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Nous passons 4 comme argument à assert_eq!, qui est égal au résultat de l'appel à add_two(2). La ligne pour ce test est test tests::it_adds_two... ok, et le texte ok indique que notre test a réussi!

Introduisons une erreur dans notre code pour voir à quoi ressemble assert_eq! lorsqu'il échoue. Changeons l'implémentation de la fonction add_two pour qu'elle ajoute 3 au lieu de 2 :

pub fn add_two(a: i32) -> i32 {
    a + 3
}

Exécutez les tests à nouveau :

exécution d'un test
test tests::it_adds_two... FAILED

échecs :

---- tests::it_adds_two sortie standard ----
1 thread'main' a généré une erreur à 'assertion failed: `(left == right)`
2   left: `4`,
3  right: `5`', src/lib.rs:11:9
note : exécutez avec la variable d'environnement `RUST_BACKTRACE=1` pour afficher
une trace de pile

échecs :
    tests::it_adds_two

résultat du test : FAILED. 0 passé ; 1 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Notre test a détecté l'erreur! Le test it_adds_two a échoué, et le message nous indique que l'assertion qui a échoué était assertion failed: (left == right)`[1] et quelles étaient les valeurs de left[2] et right[3]. Ce message nous aide à commencer le débogage : l'argument leftétait4mais l'argumentright, où nous avions add_two(2), était 5`. Vous pouvez imaginer que cela serait particulièrement utile lorsque nous avons beaucoup de tests en cours.

Notez que dans certains langages et frameworks de test, les paramètres des fonctions d'assertion d'égalité sont appelés expected et actual, et l'ordre dans lequel nous spécifions les arguments compte. Cependant, en Rust, ils sont appelés left et right, et l'ordre dans lequel nous spécifions la valeur que nous attendons et la valeur que le code produit n'a pas d'importance. Nous pourrions écrire l'assertion dans ce test comme assert_eq!(add_two(2), 4), ce qui résulterait du même message d'échec qui affiche assertion failed: (left == right)``.

La macro assert_ne! passera si les deux valeurs que nous lui donnons ne sont pas égales et échouera si elles sont égales. Cette macro est le plus utile dans les cas où nous ne sommes pas sûrs de ce que sera une valeur, mais que nous savons ce que la valeur ne devrait certainement pas être. Par exemple, si nous testons une fonction qui est garantie de modifier son entrée d'une certaine manière, mais que la manière dont l'entrée est modifiée dépend du jour de la semaine où nous exécutons nos tests, le meilleur élément à affirmer pourrait être que la sortie de la fonction n'est pas égale à l'entrée.

Sous le capot, les macros assert_eq! et assert_ne! utilisent respectivement les opérateurs == et !=. Lorsque les assertions échouent, ces macros affichent leurs arguments en utilisant le formattage de débogage, ce qui signifie que les valeurs comparées doivent implémenter les traits PartialEq et Debug. Tous les types primitifs et la plupart des types de la bibliothèque standard implémentent ces traits. Pour les structs et les énumérations que vous définissez vous-même, vous devrez implémenter PartialEq pour affirmer l'égalité de ces types. Vous devrez également implémenter Debug pour afficher les valeurs lorsque l'assertion échoue. Étant donné que les deux traits sont des traits dérivables, comme mentionné dans la liste 5-12, cela est généralement aussi simple que d'ajouter l'annotation #[derive(PartialEq, Debug)] à votre définition de struct ou d'énumération. Consultez l'annexe C pour plus de détails sur ces traits dérivables et d'autres.

Ajouter des messages d'erreur personnalisés

Vous pouvez également ajouter un message personnalisé à imprimer avec le message d'erreur en tant qu'arguments optionnels aux macros assert!, assert_eq! et assert_ne!. Tous les arguments spécifiés après les arguments requis sont transmis à la macro format! (discutée dans "Concaténation avec l'opérateur + ou la macro format!"), de sorte que vous pouvez passer une chaîne de formatage qui contient des emplacements {} et des valeurs à insérer dans ces emplacements. Les messages personnalisés sont utiles pour documenter ce qu'une assertion signifie ; lorsqu'un test échoue, vous aurez une meilleure idée du problème avec le code.

Par exemple, disons que nous avons une fonction qui salue les gens par leur nom et que nous voulons tester que le nom que nous passons à la fonction apparaît dans la sortie :

Nom de fichier : src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {name}!")
}

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

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

Les exigences de ce programme n'ont pas encore été convenues, et nous sommes assez sûrs que le texte Hello au début de la salutation changera. Nous avons décidé que nous ne voulons pas devoir mettre à jour le test lorsque les exigences changent, donc au lieu de vérifier l'égalité exacte avec la valeur renvoyée par la fonction greeting, nous allons simplement affirmer que la sortie contient le texte du paramètre d'entrée.

Maintenant, introduisons une erreur dans ce code en changeant greeting pour exclure name pour voir à quoi ressemble l'erreur de test par défaut :

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

Exécuter ce test produit ceci :

exécution d'un test
test tests::greeting_contains_name... FAILED

échecs :

---- tests::greeting_contains_name sortie standard ----
thread'main' a généré une erreur à 'assertion failed:
result.contains(\"Carol\")', src/lib.rs:12:9
note : exécutez avec la variable d'environnement `RUST_BACKTRACE=1` pour afficher
une trace de pile


échecs :
    tests::greeting_contains_name

Ce résultat indique simplement que l'assertion a échoué et sur quelle ligne se trouve l'assertion. Un message d'erreur plus utile afficherait la valeur de la fonction greeting. Ajoutons un message d'erreur personnalisé composé d'une chaîne de formatage avec un emplacement remplacé par la valeur réelle que nous avons obtenue de la fonction greeting :

#[test]
fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
        result.contains("Carol"),
        "Greeting did not contain name, value was `{result}`"
    );
}

Maintenant, lorsque nous exécutons le test, nous obtiendrons un message d'erreur plus informatif :

---- tests::greeting_contains_name sortie standard ----
thread'main' a généré une erreur à 'Greeting did not contain name, value
was `Hello!`', src/lib.rs:12:9
note : exécutez avec la variable d'environnement `RUST_BACKTRACE=1` pour afficher
une trace de pile

Nous pouvons voir la valeur que nous avons effectivement obtenue dans la sortie du test, ce qui nous aiderait à déboguer ce qui s'est passé au lieu de ce que nous nous attendions à ce qui se passe.

Vérifier les panics avec should_panic

En plus de vérifier les valeurs de retour, il est important de vérifier que notre code gère les conditions d'erreur comme nous le souhaitons. Par exemple, considérons le type Guess que nous avons créé dans la liste 9-13. Autre code qui utilise Guess dépend de la garantie que les instances de Guess ne contiendront que des valeurs comprises entre 1 et 100. Nous pouvons écrire un test qui assure que l'essai de création d'une instance de Guess avec une valeur en dehors de cette plage provoque une panique.

Nous le faisons en ajoutant l'attribut should_panic à notre fonction de test. Le test passe si le code à l'intérieur de la fonction provoque une panique ; le test échoue si le code à l'intérieur de la fonction ne provoque pas de panique.

La liste 11-8 montre un test qui vérifie que les conditions d'erreur de Guess::new se produisent lorsque nous les attendons.

// src/lib.rs
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Liste 11-8 : Vérification qu'une condition entraînera une panique!

Nous plaçons l'attribut #[should_panic] après l'attribut #[test] et avant la fonction de test à laquelle il s'applique. Voyons le résultat lorsque ce test passe :

exécution d'un test
test tests::greater_than_100 - devrait provoquer une panique... ok

résultat du test : ok. 1 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Cela semble bon! Maintenant, introduisons une erreur dans notre code en supprimant la condition selon laquelle la fonction new provoquera une panique si la valeur est supérieure à 100 :

// src/lib.rs
--extrait--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

Lorsque nous exécutons le test de la liste 11-8, il échouera :

exécution d'un test
test tests::greater_than_100 - devrait provoquer une panique... FAILED

échecs :

---- tests::greater_than_100 sortie standard ----
note : le test n'a pas provoqué de panique comme attendu

échecs :
    tests::greater_than_100

résultat du test : FAILED. 0 passé ; 1 échoué ; 0 ignoré ; 0 mesuré ; 0
filtré ; terminé en 0,00 s

Dans ce cas, nous ne recevons pas un message très utile, mais en regardant la fonction de test, nous voyons qu'elle est annotée avec #[should_panic]. L'erreur que nous avons obtenue signifie que le code dans la fonction de test n'a pas entraîné de panique.

Les tests qui utilisent should_panic peuvent être imprécis. Un test should_panic passerait même si le test provoque une panique pour une raison différente de celle que nous attendions. Pour rendre les tests should_panic plus précis, nous pouvons ajouter un paramètre optionnel expected à l'attribut should_panic. Le harness de test s'assurera que le message d'erreur contient le texte fourni. Par exemple, considérez le code modifié de Guess dans la liste 11-9 où la fonction new provoque une panique avec des messages différents selon que la valeur est trop petite ou trop grande.

// src/lib.rs
--extrait--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Liste 11-9 : Vérification d'une panique! avec un message de panique contenant une sous-chaîne spécifiée

Ce test passera car la valeur que nous avons placée dans le paramètre expected de l'attribut should_panic est une sous-chaîne du message avec lequel la fonction Guess::new provoque une panique. Nous aurions pu spécifier le message de panique entier que nous attendons, qui dans ce cas serait Guess value must be less than or equal to 100, got 200. Ce que vous choisissez de spécifier dépend de la partie du message de panique qui est unique ou dynamique et de la précision que vous voulez donner à votre test. Dans ce cas, une sous-chaîne du message de panique suffit pour s'assurer que le code dans la fonction de test exécute le cas else if value > 100.

Pour voir ce qui se passe lorsqu'un test should_panic avec un message expected échoue, introduisons à nouveau une erreur dans notre code en échangeant les corps des blocs if value < 1 et else if value > 100 :

// src/lib.rs
--extrait--
if value < 1 {
    panic!(
        "Guess value must be less than or equal to 100, got {}.",
        value
    );
} else if value > 100 {
    panic!(
        "Guess value must be greater than or equal to 1, got {}.",
        value
    );
}
--extrait--

Cette fois, lorsque nous exécutons le test should_panic, il échouera :

exécution d'un test
test tests::greater_than_100 - devrait provoquer une panique... FAILED

échecs :

---- tests::greater_than_100 sortie standard ----
thread'main' a généré une erreur à 'Guess value must be greater than or equal to 1, got
200.', src/lib.rs:13:13
note : exécutez avec la variable d'environnement `RUST_BACKTRACE=1` pour afficher une trace de pile
note : la panique ne contenait pas la chaîne attendue
      message de panique : `"Guess value must be greater than or equal to 1, got
200."`,
 sous-chaîne attendue : `"less than or equal to 100"`

échecs :
    tests::greater_than_100

résultat du test : FAILED. 0 passé ; 1 échoué ; 0 ignoré ; 0 mesuré ; 0 filtré ;
terminé en 0,00 s

Le message d'erreur indique que ce test a effectivement provoqué une panique comme nous l'attendions, mais le message de panique n'a pas inclus la chaîne attendue 'Guess value must be less than or equal to 100'. Le message de panique que nous avons obtenu dans ce cas était Guess value must be greater than or equal to 1, got 200. Maintenant, nous pouvons commencer à localiser notre bogue!

Utiliser Result<T, E> dans les tests

Jusqu'à présent, nos tests ont tous provoqué une panique lorsqu'ils ont échoué. Nous pouvons également écrire des tests qui utilisent Result<T, E>! Voici le test de la liste 11-1, réécrit pour utiliser Result<T, E> et renvoyer une Err au lieu de provoquer une panique :

Nom de fichier : src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

La fonction it_works a maintenant le type de retour Result<(), String>. Dans le corps de la fonction, au lieu d'appeler la macro assert_eq!, nous renvoyons Ok(()) lorsque le test passe et une Err avec une String à l'intérieur lorsque le test échoue.

Écrire des tests de manière à ce qu'ils renvoient un Result<T, E> vous permet d'utiliser l'opérateur question dans le corps des tests, ce qui peut être un moyen pratique d'écrire des tests qui devraient échouer si une opération quelconque à l'intérieur d'eux renvoie une variante Err.

Vous ne pouvez pas utiliser l'annotation #[should_panic] sur des tests qui utilisent Result<T, E>. Pour affirmer qu'une opération renvoie une variante Err, n'utilisez pas l'opérateur question sur la valeur Result<T, E>. Au lieu de cela, utilisez assert!(value.is_err()).

Maintenant que vous connaissez plusieurs façons d'écrire des tests, voyons ce qui se passe lorsque nous exécutons nos tests et explorons les différentes options que nous pouvons utiliser avec cargo test.

Sommaire

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