Comment écrire des tests
Les tests sont des fonctions Rust qui vérifient que le code non-test fonctionne de la maniere 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ère pour nous. Ce module vous fournit un modèle pour écrire vos tests afin que vous n’ayez pas a rechercher la structure et la syntaxe exactes chaque fois que vous demarrez un nouveau projet. Vous pouvez ajouter autant de fonctions de test et de modules de test supplementaires 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.
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);
}
}
cargo newLe 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’executeur 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 a mettre en place des scenarios 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 egal à 4. Cette assertion sert d’exemple de format pour un test typique. Executons-le pour voir que ce test reussit.
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
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”][ignoring] 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”][subset]. 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 redaction de ce livre, disponibles que dans la version nightly de Rust. Consultez [la documentation sur les tests de performance][bench] pour en savoir plus.
La partie suivante de la sortie des tests, commencant 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 a garder votre documentation et votre code synchronises ! Nous verrons comment écrire des tests de documentation dans la section [“Les commentaires de documentation comme tests”][doc-comments] du chapitre 14. Pour l’instant, nous ignorerons la sortie Doc-tests.
Commencons a 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 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 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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-01-changing-test-name/output.txt}}
$ 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 echoue ! Les tests echouent quand quelque chose dans la fonction de test provoque un panic. Chaque test est exécute dans un nouveau thread, et quand le thread principal voit qu’un thread de test est mort, le test est marque comme echoue. Au chapitre 9, nous avons parle de la façon la plus simple de provoquer un panic : appeler la macro panic!. Entrez le nouveau test sous forme de fonction nommee another, pour que votre fichier src/lib.rs ressemble à l’encart 11-3.
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");
}
}
panic! macroExé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 echoue.
$ 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' 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`
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”][controlling-how-tests-are-run].
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’evalue a true. Nous donnons à la macro assert! un argument qui s’evalue en un booleen. Si la valeur est true, rien ne se passe et le test reussit. Si la valeur est false, la macro assert! appelle panic! pour faire echouer le test. Utiliser la macro assert! nous aide a vérifier que notre code fonctionne de la maniere prevue.
Au chapitre 5, dans l’encart 5-15, nous avons utilise une structure Rectangle et une methode 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!.
#[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
}
}
Rectangle struct and its can_hold method from Chapter 5La methode can_hold retourné un booleen, 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 methode can_hold en creant une instance de Rectangle ayant une largeur de 8 et une hauteur de 7, et en verifiant qu’elle peut contenir une autre instance de Rectangle ayant une largeur de 5 et une hauteur de 1.
#[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));
}
}
can_hold that checks whether a larger rectangle can indeed hold a smaller rectanglesection. Because the tests module is an inner module, we need to bring the code under test in the outer module into the scope of the inner module. We use a glob here, so anything we define in the outer module is available to this tests module. –> 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”][paths-for-referring-to-an-item-in-the-module-tree]. 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 ! console {{#include ../listings/ch11-writing-automated-tests/listing-11-06/output.txt}}
$ 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 reussit ! Ajoutons un autre test, cette fois en verifiant qu’un plus petit rectangle ne peut pas contenir un plus grand rectangle :
Fichier : src/lib.rs rust,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-01/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 consequence, notre test reussira si can_hold retourné false : console {{#include ../listings/ch11-writing-automated-tests/no-listing-02-adding-another-rectangle-test/output.txt}}
$ 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 reussissent ! Maintenant, voyons ce qui arrive a nos résultats de tests quand nous introduisons un bogue dans notre code. Nous allons changer l’implémentation de la methode can_hold en remplacant le signé supérieur (>) par un signé inférieur (<) lors de la comparaison des largeurs : rust,not_desired_behavior,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-03-introducing-a-bug/src/lib.rs:here}}
#[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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-03-introducing-a-bug/output.txt}}
$ 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' 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’egalite 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 nommee add_two qui ajouté 2 à son paramètre, puis nous testons cette fonction en utilisant la macro assert_eq!.
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);
}
}
add_two using the assert_eq! macroVerifions que ca reussit ! console {{#include ../listings/ch11-writing-automated-tests/listing-11-07/output.txt}}
$ 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 nommee 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 a quoi ressemble assert_eq! quand il echoue. Changez l’implémentation de la fonction add_two pour qu’elle ajouté 3 à la place : rust,not_desired_behavior,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-04-bug-in-add-two/src/lib.rs:here}}
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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-04-bug-in-add-two/output.txt}}
$ 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' 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’egalite sont appelés expected et actual, et l’ordre dans lequel nous specifiions les arguments a de l’importance. Cependant, en Rust, ils sont appelés left et right, et l’ordre dans lequel nous specifiions 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 echouent, ces macros affichent leurs arguments en utilisant le formatage de debogage, ce qui signifie que les valeurs comparees doivent implémenter les traits PartialEq et Debug. Tous les types primitifs et la plupart des types de la bibliothèque standard implementent ces traits. Pour les structures et les énumérations que vous définissez vous-meme, vous devrez implémenter PartialEq pour vérifier l’egalite de ces types. Vous devrez aussi implémenter Debug pour afficher les valeurs quand l’assertion echoue. Comme les deux traits sont derivables, 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 derivables”][derivable-traits], pour plus de détails sur ces traits derivables et les autres.
Ajouter des messages d’échec personnalises
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!”][concatenating] 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 rust,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/listing-11-01/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 : rust,not_desired_behavior,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/src/lib.rs:here}}
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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/output.txt}}
$ 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' 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 : rust,ignore {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-07-custom-failure-message/src/lib.rs:here}}
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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-07-custom-failure-message/output.txt}}
$ 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' 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.
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);
}
}
panic!Nous placons l’attribut #[should_panic] après l’attribut #[test] et avant la fonction de test a laquelle il s’applique. Regardons le résultat quand ce test reussit : console {{#include ../listings/ch11-writing-automated-tests/listing-11-08/output.txt}}
$ 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
Ca à 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 : rust,not_desired_behavior,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-08-guess-with-bug/src/lib.rs:here}}
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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-08-guess-with-bug/output.txt}}
$ 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 annotee avec #[should_panic]. L’échec que nous avons obtenu signifie que le code dans la fonction de test n’a pas provoque de panic.
Les tests utilisant should_panic peuvent être imprecis. Un test should_panic reussirait 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 ou la fonction new provoque un panic avec des messages différents selon que la valeur est trop petite ou trop grande.
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);
}
}
panic! with a panic message containing a specified substringCe test reussira parce que la valeur que nous avons mise dans le paramètre expected de l’attribut should_panic est une sous-chaine 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 depend de la quantite du message de panic qui est unique ou dynamique et de la precision que vous souhaitez pour votre test. Dans ce cas, une sous-chaine 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 : rust,ignore,not_desired_behavior {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-09-guess-with-panic-msg-bug/src/lib.rs:here}}
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 : console {{#include ../listings/ch11-writing-automated-tests/no-listing-09-guess-with-panic-msg-bug/output.txt}}
$ 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' 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 : rust,noplayground {{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-10-result-in-tests/src/lib.rs:here}}
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 reussit et un Err contenant un String quand le test echoue.
É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 echouer si une opération en leur sein retourné 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 retourné une variante Err, n’utilisez pas l’opérateur point d’interrogation sur la valeur Result<T, E>. Utilisez plutot 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.