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

Comment écrire des tests

Les tests sont des fonctions Rust qui vérifient que le code non-test fonctionne de la manière attendue. Le corps des fonctions de test effectue généralement ces trois actions :

  • Mettre en place les données ou l’état nécessaires.
  • Exécuter le code que vous souhaitez tester.
  • Vérifier que les résultats sont ceux que vous attendez.

Examinons les fonctionnalités que Rust fournit specifiquement pour écrire des tests qui effectuent ces actions, notamment l’attribut test, quelques macros et l’attribut should_panic.

Structurer les fonctions de test

Dans sa forme la plus simple, un test en Rust est une fonction annotee avec l’attribut test. Les attributs sont des metadonnees sur des portions de code Rust ; un exemple est l’attribut derive que nous avons utilise avec les structures au chapitre 5. Pour transformer une fonction en fonction de test, ajoutez #[test] sur la ligne avant fn. Quand vous exécutez vos tests avec la commande cargo test, Rust compilé un binaire d’exécution de tests qui lance les fonctions annotees et rapporte si chaque fonction de test reussit ou echoue.

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 fournit un modèle pour écrire vos tests afin que vous n’ayez pas à rechercher la structure et la syntaxe exactes chaque fois que vous démarrez un nouveau projet. Vous pouvez ajouter autant de fonctions de test et de modules de test supplémentaires que vous le souhaitez !

Nous allons explorer certains aspects du fonctionnement des tests en experimentant avec le test modèle avant de tester réellement du code. Ensuite, nous ecrirons des tests concrets qui appellent du code que nous avons écrit et vérifient que son comportement est correct.

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

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

Le contenu du fichier src/lib.rs de votre bibliothèque adder devrait ressembler à l’encart 11-1.

Filename: 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);
    }
}
Listing 11-1: The code generated automatically by cargo new

Le fichier commence par un exemple de fonction add afin que nous ayons quelque chose a tester.

Pour l’instant, concentrons-nous uniquement sur la fonction it_works. Notez l’annotation #[test] : cet attribut indique qu’il s’agit d’une fonction de test, de sorte que l’exécuteur de tests sait qu’il doit traiter cette fonction comme un test. Nous pourrions aussi avoir des fonctions non-test dans le module tests pour aider à mettre en place des scénarios communs ou effectuer des opérations courantes, c’est pourquoi nous devons toujours indiquer quelles fonctions sont des tests.

Le corps de la fonction d’exemple utilise la macro assert_eq! pour vérifier que result, qui contient le résultat de l’appel de add avec 2 et 2, est égal à 4. Cette assertion sert d’exemple de format pour un test typique. Exécutons-le pour voir que ce test réussit.

La commande cargo test exécute tous les tests de notre projet, comme montre dans l’encart 11-2.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)

running 1 test
test tests::it_works ... 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

Listing 11-2: The output from running the automatically generated test

Cargo a compilé et exécute le test. Nous voyons la ligne running 1 test. La ligne suivante montre le nom de la fonction de test générée, appelée tests::it_works, et que le résultat de l’exécution de ce test est ok. Le resume global test result: ok. signifie que tous les tests ont réussi, et la partie qui indique 1 passed; 0 failed totalise le nombre de tests qui ont réussi ou echoue.

Il est possible de marquer un test comme ignore afin qu’il ne s’exécute pas dans un cas particulier ; nous aborderons cela dans la section “Ignorer des tests sauf demande explicite” plus loin dans ce chapitre. Comme nous ne l’avons pas fait ici, le resume affiche 0 ignored. Nous pouvons également passer un argument à la commande cargo test pour n’exécuter que les tests dont le nom correspond à une chaîne de caractères ; c’est ce qu’on appelle le filtrage, et nous l’aborderons dans la section “Exécuter un sous-ensemble de tests par nom”. Ici, nous n’avons pas filtre les tests exécutés, donc la fin du resume affiche 0 filtered out.

La statistique 0 measured concerne les tests de performance (benchmarks) qui mesurent les performances. Les tests de performance ne sont, au moment de la rédaction de ce livre, disponibles que dans la version nightly de Rust. Consultez la documentation sur les tests de performance pour en savoir plus.

La partie suivante de la sortie des tests, commençant par Doc-tests adder, concerne les résultats des 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 d’API. Cette fonctionnalité aide à garder votre documentation et votre code synchronisés ! Nous verrons comment écrire des tests de documentation dans la section “Les commentaires de documentation comme tests” du chapitre 14. Pour l’instant, nous ignorerons la sortie Doc-tests.

Commençons à personnaliser le test selon nos besoins. D’abord, changez le nom de la fonction it_works pour un nom différent, comme exploration, comme ceci :

Fichier : src/lib.rs

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

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

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

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

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::exploration ... 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

Maintenant, nous allons ajouter un autre test, mais cette fois nous allons créer un test qui échoue ! Les tests échouent quand quelque chose dans la fonction de test provoque un panic. Chaque test est exécuté dans un nouveau thread, et quand le thread principal voit qu’un thread de test est mort, le test est marqué comme échoué. Au chapitre 9, nous avons parlé de la façon la plus simple de provoquer un panic : appeler la macro panic!. Entrez le nouveau test sous forme de fonction nommée another, pour que votre fichier src/lib.rs ressemble à l’encart 11-3.

Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

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

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}
Listing 11-3: Adding a second test that will fail because we call the panic! macro

Exécutez les tests à nouveau avec cargo test. La sortie devrait ressembler à l’encart 11-4, qui montre que notre test exploration a réussi et que another a échoué.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----

thread 'tests::another' (6019162) panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

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

error: test failed, to rerun pass `--lib`
Listing 11-4: Test results when one test passes and one test fails

Au lieu de ok, la ligne test tests::another affiche FAILED. Deux nouvelles sections apparaissent entre les résultats individuels et le resume : la première affiche la raison détaillée de chaque échec de test. Dans ce cas, nous obtenons les détails indiquant que tests::another a echoue parce qu’il a provoque un panic avec le message Make this test fail à la ligne 17 du fichier src/lib.rs. La section suivante liste uniquement les noms de tous les tests en échec, ce qui est utile quand il y a beaucoup de tests et beaucoup de sorties detaillees de tests en échec. Nous pouvons utiliser le nom d’un test en échec pour n’exécuter que ce test et le deboguer plus facilement ; nous parlerons davantage des façons d’exécuter les tests dans la section “Controler comment les tests sont exécutés”.

La ligne de resume s’affiche à la fin : globalement, notre résultat de test est FAILED. Nous avions un test réussi et un test en échec.

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

Vérifier les résultats avec assert!

La macro assert!, fournie par la bibliothèque standard, est utile quand vous voulez vous assurer qu’une certaine condition dans un test s’évalue à true. Nous donnons à la macro assert! un argument qui s’évalue en un booléen. Si la valeur est true, rien ne se passe et le test réussit. Si la valeur est false, la macro assert! appelle panic! pour faire échouer le test. Utiliser la macro assert! nous aide à vérifier que notre code fonctionne de la manière prévue.

Au chapitre 5, dans l’encart 5-15, nous avons utilisé une structure Rectangle et une méthode can_hold, qui sont reproduites ici dans l’encart 11-5. Mettons ce code dans le fichier src/lib.rs, puis écrivons quelques tests en utilisant la macro assert!.

Filename: 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
    }
}
Listing 11-5: The Rectangle struct and its can_hold method from Chapter 5

La méthode can_hold retourne un booléen, ce qui en fait un cas d’utilisation parfait pour la macro assert!. Dans l’encart 11-6, nous écrivons un test qui exerce la méthode can_hold en créant une instance de Rectangle ayant une largeur de 8 et une hauteur de 7, et en vérifiant qu’elle peut contenir une autre instance de Rectangle ayant une largeur de 5 et une hauteur de 1.

Filename: 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
    }
}

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

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

        assert!(larger.can_hold(&smaller));
    }
}
Listing 11-6: A test for can_hold that checks whether a larger rectangle can indeed hold a smaller rectangle

Notez la ligne use super::*; à l’intérieur du module tests. Le module tests est un module ordinaire qui suit les règles de visibilite habituelles que nous avons couvertes au chapitre 7 dans la section “Les chemins pour faire référence à un élément dans l’arborescence des modules”. Comme le module tests est un module interne, nous devons importer le code a tester du 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 dans ce module tests.

Nous avons nomme notre test larger_can_hold_smaller, et nous avons crée les deux instances de Rectangle dont nous avons besoin. Ensuite, nous avons appelé la macro assert! en lui passant le résultat de l’appel larger.can_hold(&smaller). Cette expression est censee retourner true, donc notre test devrait réussir. Voyons cela !

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok

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

   Doc-tests rectangle

running 0 tests

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

Il réussit ! Ajoutons un autre test, cette fois en vérifiant qu’un plus petit rectangle ne peut pas contenir un plus grand rectangle :

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
    }
}

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

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

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

    #[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 inverser ce résultat avant de le passer à la macro assert!. En conséquence, notre test réussira si can_hold retourne false :

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok

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

   Doc-tests rectangle

running 0 tests

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

Deux tests qui réussissent ! Maintenant, voyons ce qui arrive à nos résultats de tests quand nous introduisons un bogue dans notre code. Nous allons changer l’implémentation de la méthode can_hold en remplaçant le signe supérieur (>) par un signe inférieur (<) lors de la comparaison des largeurs :

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

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

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

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

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

    #[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));
    }
}

L’exécution des tests produit maintenant le résultat suivant :

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok

failures:

---- tests::larger_can_hold_smaller stdout ----

thread 'tests::larger_can_hold_smaller' (6020788) panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::larger_can_hold_smaller

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

error: test failed, to rerun pass `--lib`

Nos tests ont detecte le bogue ! Comme larger.width vaut 8 et smaller.width vaut 5, la comparaison des largeurs dans can_hold retourné maintenant false : 8 n’est pas inférieur à 5.

Tester l’égalité avec assert_eq! et assert_ne!

Une façon courante de vérifier le fonctionnement est de tester l’egalite entre le résultat du code teste et la valeur que vous attendez du code. Vous pourriez le faire en utilisant la macro assert! et en lui passant une expression utilisant l’opérateur ==. Cependant, c’est un test si courant que la bibliothèque standard fournit une paire de macros – assert_eq! et assert_ne! – pour effectuer ce test plus facilement. Ces macros comparent deux arguments pour l’egalite ou l’inegalite, respectivement. Elles affichent également les deux valeurs si l’assertion echoue, ce qui permet de voir plus facilement pourquoi le test a echoue ; à l’inverse, la macro assert! indique seulement qu’elle a obtenu une valeur false pour l’expression ==, sans afficher les valeurs qui ont mene à la valeur false.

Dans l’encart 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!.

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

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

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}
Listing 11-7: Testing the function add_two using the assert_eq! macro

Vérifions que ça réussit !

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::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

Nous créons une variable nommée result qui contient le résultat de l’appel de add_two(2). Ensuite, nous passons result et 4 comme arguments à la macro assert_eq!. La ligne de sortie pour ce test est test tests::it_adds_two ... ok, et le texte ok indique que notre test a réussi !

Introduisons un bogue dans notre code pour voir à quoi ressemble assert_eq! quand il échoue. Changez l’implémentation de la fonction add_two pour qu’elle ajoute 3 à la place :

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

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

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

Exécutez les tests à nouveau :

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----

thread 'tests::it_adds_two' (6020955) panicked at src/lib.rs:12:9:
assertion `left == right` failed
  left: 5
 right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_adds_two

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

error: test failed, to rerun pass `--lib`

Notre test a detecte le bogue ! Le test tests::it_adds_two a echoue, et le message nous indique que l’assertion qui a echoue était left == right et quelles sont les valeurs de left et right. Ce message nous aide a commencer le debogage : l’argument left, ou nous avions le résultat de l’appel de add_two(2), était 5, mais l’argument right était 4. Vous pouvez imaginer que cela serait particulierement utile quand 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 a de l’importance. Cependant, en Rust, ils sont appelés left et right, et l’ordre dans lequel nous spécifions la valeur attendue et la valeur produite par le code n’a pas d’importance. Nous pourrions écrire l’assertion dans ce test comme assert_eq!(4, result), ce qui produirait le même message d’échec affichant assertion `left == right` failed.

La macro assert_ne! reussira si les deux valeurs que nous lui donnons ne sont pas egales et echouera si elles sont egales. Cette macro est surtout utile dans les cas où nous ne savons pas quelle valeur quelque chose aura, mais nous savons quelle valeur elle ne devrait definitivement pas avoir. Par exemple, si nous testons une fonction qui est garantie de modifier son entrée d’une certaine façon, mais la façon dont l’entrée est modifiée depend du jour de la semaine ou nous exécutons nos tests, la meilleure chose a vérifier pourrait être que la sortie de la fonction n’est pas egale à l’entrée.

Sous la surface, les macros assert_eq! et assert_ne! utilisent respectivement les opérateurs == et !=. Quand les assertions échouent, ces macros affichent leurs arguments en utilisant le formatage 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 structures et les énumérations que vous définissez vous-même, vous devrez implémenter PartialEq pour vérifier l’égalité de ces types. Vous devrez aussi implémenter Debug pour afficher les valeurs quand l’assertion échoue. Comme les deux traits sont dérivables, comme mentionné dans l’encart 5-12 au chapitre 5, c’est généralement aussi simple que d’ajouter l’annotation #[derive(PartialEq, Debug)] à votre définition de structure ou d’énumération. Voir l’annexe C, “Les traits dérivables”, pour plus de détails sur ces traits dérivables et les autres.

Ajouter des messages d’échec personnalisés

Vous pouvez aussi ajouter un message personnalise a afficher avec le message d’échec en tant qu’arguments optionnels des macros assert!, assert_eq! et assert_ne!. Tous les arguments spécifiés après les arguments requis sont passes à la macro format! (discutee dans “La concatenation avec + ou format! au chapitre 8), vous pouvez donc passer une chaîne de format contenant des emplacements {} et des valeurs a y inserer. Les messages personnalises sont utiles pour documenter ce que signifie une assertion ; quand un test echoue, vous aurez une meilleure idee du problème dans le code.

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

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é définies, et nous sommes assez surs que le texte Hello au début du message de bienvenue changera. Nous avons decide que nous ne voulons pas avoir a mettre à jour le test quand les exigences changent, donc au lieu de vérifier l’egalite exacte avec la valeur retournée par la fonction greeting, nous allons simplement vérifier que la sortie contient le texte du paramètre d’entrée.

Maintenant, introduisons un bogue dans ce code en modifiant greeting pour exclure name afin de voir a quoi ressemble un échec de test par défaut :

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

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

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

L’exécution de ce test produit le résultat suivant :

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' (6021143) panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

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

error: test failed, to rerun pass `--lib`

Ce résultat indique seulement que l’assertion a echoue et sur quelle ligne se trouve l’assertion. Un message d’échec plus utile afficherait la valeur de la fonction greeting. Ajoutons un message d’échec personnalise compose d’une chaîne de format avec un emplacement rempli par la valeur réelle que nous avons obtenue de la fonction greeting :

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

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

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

Maintenant, quand nous exécutons le test, nous obtenons un message d’erreur plus informatif :

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' (6021333) panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

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

error: test failed, to rerun pass `--lib`

Nous pouvons voir la valeur que nous avons réellement obtenue dans la sortie du test, ce qui nous aide a deboguer ce qui s’est passe au lieu de ce que nous nous attendions a voir se produire.

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 l’attendons. Par exemple, considérez le type Guess que nous avons crée au chapitre 9, dans l’encart 9-13. Le code qui utilise Guess depend de la garantie que les instances de Guess ne contiendront que des valeurs entre 1 et 100. Nous pouvons écrire un test qui s’assuré que tenter de créer une instance de Guess avec une valeur en dehors de cette plage provoque un panic.

Nous faisons cela en ajoutant l’attribut should_panic à notre fonction de test. Le test reussit si le code à l’intérieur de la fonction provoque un panic ; le test echoue si le code à l’intérieur de la fonction ne provoque pas de panic.

L’encart 11-8 montre un test qui vérifie que les conditions d’erreur de Guess::new se produisent quand nous nous y attendons.

Filename: 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);
    }
}
Listing 11-8: Testing that a condition will cause a panic!

Nous plaçons l’attribut #[should_panic] après l’attribut #[test] et avant la fonction de test à laquelle il s’applique. Regardons le résultat quand ce test réussit :

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... ok

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

   Doc-tests guessing_game

running 0 tests

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

Ça a l’air bon ! Maintenant, introduisons un bogue dans notre code en supprimant la condition qui fait que la fonction new provoque un panic si la valeur est supérieure à 100 :

pub struct Guess {
    value: i32,
}

// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            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);
    }
}

Quand nous exécutons le test de l’encart 11-8, il echouera :

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8

failures:
    tests::greater_than_100

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

error: test failed, to rerun pass `--lib`

Nous n’obtenons pas un message très utile dans ce cas, mais quand nous regardons la fonction de test, nous voyons qu’elle est annotée avec #[should_panic]. L’échec que nous avons obtenu signifie que le code dans la fonction de test n’a pas provoqué de panic.

Les tests utilisant should_panic peuvent être imprécis. Un test should_panic réussirait même si le test provoque un panic 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 harnais de test s’assurera que le message d’échec contient le texte fourni. Par exemple, considérez le code modifié pour Guess dans l’encart 11-9 où la fonction new provoque un panic avec des messages différents selon que la valeur est trop petite ou trop grande.

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

// --snip--

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);
    }
}
Listing 11-9: Testing for a panic! with a panic message containing a specified substring

Ce test réussira parce que la valeur que nous avons mise dans le paramètre expected de l’attribut should_panic est une sous-chaîne du message avec lequel la fonction Guess::new provoque un panic. Nous aurions pu spécifier le message de panic complet que nous attendions, 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 quantité du message de panic qui est unique ou dynamique et de la précision que vous souhaitez pour votre test. Dans ce cas, une sous-chaîne du message de panic 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 quand un test should_panic avec un message expected echoue, introduisons à nouveau un bogue dans notre code en echangeant les corps des blocs if value < 1 et else if value > 100 :

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        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}."
            );
        }

        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);
    }
}

Cette fois, quand nous exécutons le test should_panic, il echouera :

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----

thread 'tests::greater_than_100' (6021675) panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: "Guess value must be greater than or equal to 1, got 200."
 expected substring: "less than or equal to 100"

failures:
    tests::greater_than_100

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

error: test failed, to rerun pass `--lib`

Le message d’échec indique que ce test a bien provoque un panic comme nous l’attendions, mais le message de panic ne contenait pas la chaîne attendue less than or equal to 100. Le message de panic que nous avons obtenu dans ce cas était Guess value must be greater than or equal to 1, got 200. Nous pouvons maintenant commencer a chercher ou se trouve notre bogue !

Utiliser Result<T, E> dans les tests

Tous nos tests jusqu’ici provoquent un panic quand ils echouent. Nous pouvons aussi écrire des tests qui utilisent Result<T, E> ! Voici le test de l’encart 11-1, reecrit pour utiliser Result<T, E> et retourner un Err au lieu de provoquer un panic :

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

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

    #[test]
    fn it_works() -> Result<(), String> {
        let result = add(2, 2);

        if result == 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 retournons Ok(()) quand le test réussit et un Err contenant un String quand le test échoue.

Écrire des tests qui retournent un Result<T, E> vous permet d’utiliser l’opérateur point d’interrogation dans le corps des tests, ce qui peut être une façon pratique d’écrire des tests qui devraient échouer si une opération en leur sein retourne une variante Err.

Vous ne pouvez pas utiliser l’annotation #[should_panic] sur les tests qui utilisent Result<T, E>. Pour vérifier qu’une opération retourne une variante Err, n’utilisez pas l’opérateur point d’interrogation sur la valeur Result<T, E>. Utilisez plutôt assert!(value.is_err()).

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