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

Utiliser les tests unitaires

En complément des exemples de la documentation destinée à aux utilisateurs·trices de notre API, nous pouvons intégrer des tests unitaires plus poussés dans nos codes sources.

Ici, par exemple, nous allons ajouter un test unitaire pour vérifier le comportements aux limites que j’ai survolé dans mon code en choisissant le type u64 en sortie de fonction.

Pour cela, nous allons ajouter un module de test à la suite de notre fonction. C’est la convention en Rust : nous disposons les tests unitaires dans le même fichier que le code source testé.

Code source pour implémenter le test

#![allow(unused)]
fn main() {
/// [Documentation avec exemple…]
pub fn calculer_perimetre(cote: u32) -> u64 {
    (cote as u64) * 4_u64
}

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

    #[test]
    fn test_limite_debordement() {
        // Le nombre maximum pour un u32 est 4_294_967_295.
        // Divisé par 4, cela donne 1_073_741_823.
        // Si nous passons une longueur de côté supérieure à cette valeur, 
        // que se passe-t-il à l'exécution ?
        let resultat = calculer_perimetre(1_073_741_824);
        assert_eq!(resultat, 4_294_967_296);
    }
}
}
Auto-critique sereine

Bien que robuste, le code que j’ai proposé pour sensibiliser à la réflexion sur les types et les conditions aux limites n’est pas top. Idem pour ce test qui n’a pas grand intérêt.

Dans des conditions plus réalistes, nous ajouterions un test pour vérifier que nous obtenons le bon résultat si nous passons en argument le plus grand entier u_32 à notre fonction (4_294_967_295).

Dans des conditions encore plus réalistes, nous travaillerions dans un espace de données cohérent (tout en u32 ou tout en u[…]), et :

  • nous ajouterions des comportements dans notre fonction de calcul pour traiter les cas d’exceptions ;
  • c’est sur la vérification du bon fonctionnement de ces traitements que nous écririons des tests unitaires.

Le test que nous avons écrit existerait (si nous choisissions de travailler en u32), mais cette fois pour nous assurer que la fonction retourne une erreur, et la bonne, plutôt qu’un résultat erroné.

Description détaillée

#[cfg(test)]

C’est un attribut de compilation conditionnelle.

  • cfg signifie configuration.
  • Cet attribut indique au compilateur Rust que tout ce qui suit (ici, le module tests) ne doit être compilé et exécuté que lorsque l’on lance la commande cargo test.
  • Lorsque nous compilons notre projet pour la production avec cargo build --release par exemple, les blocs de tests sont ignorés. Cela évite d’alourdir le binaire final inutilement avec du code de tests.

mod tests { ... }

C’est la déclaration d’un sous-module nommé tests.

  • En Rust, la convention veut que l’on isole les tests unitaires dans leur propre module.
  • Ce module crée un nouvel espace de noms (namespace) étanche, ce qui évite de mélanger les fonctions de tests avec les fonctions de notre API.

use super::calculer_perimetre;

Nous importons notre fonction dans notre module de tests. super est un mot clef permettant de remonter au niveau supérieur (par rapport à notre sous-module de test).

#[test]

C’est l’attribut de test.

  • Il sert de marqueur pour le gestionnaire de tests de Rust (test runner).
  • Lorsqu’on tape cargo test, Rust parcourt le code à la recherche de cet attribut et sait qu’il doit exécuter la fonction située juste en dessous.

fn test_limite_debordement() { ... }

C’est une fonction classique qui contient le scénario de test.

  • Par convention, on lui donne un nom très descriptif (souvent préfixé par test_) pour l’identifier dans la console qui affiche les résultats de tests.
  • Les fonctions de test ne prennent jamais de paramètres et ne retournent généralement rien.

let resultat = calculer_perimetre(1_073_741_824);

C’est l’action.

  • On appelle la fonction à tester avec la valeur limite déterminée mathématiquement (1_073_741_824).
  • Le résultat est stocké dans la variable resultat.

assert_eq!(resultat, 4_294_967_296);

C’est la vérification (l’assertion).

  • assert_eq! est une macro (reconnaissable à son !) qui signifie assert equal (vérifier l’égalité).
  • Elle compare le premier argument (resultat) avec le second argument attendu (4\_294\_967\_296).
  • Si les deux valeurs sont égales, le test passe. S’il y a la moindre différence ou si le code panique avant, le test échoue et Rust affiche explicitement la valeur obtenue face à la valeur attendue dans le retour.

Documentation

Comme pour l’ensemble de nos codes sources, nous prenons soin de documenter notre test en expliquant et explicitant tout ce qui doit l’être.

Nous le faisons avec des « commentaires » simples (//) : les tests, même s’ils complètent la documentation du code utilement pour les développeurs·ses et mainteneurs·ses de ce code, n’ont pas vocation à apparaître dans la documentation qui sert, elle, aux consommateurs·trices de notre API.

Nous pouvons remarquer et souligner que les tests unitaires constituent une part importante et pertinente de documentation des codes sources. Ils donnent des informations critiques (ici, le comportement aux limites).

Le fait que ces tests soient intégrés dans le code source AVEC ce code source me semble très utile : en tant que consommateur de codes écrits par d’autres, j’apprécie de pouvoir me faire une opinion des points d’attention que les développeurs·ses ont listés et traités (ou omis voire délaissés !) dans leurs tests unitaires.

Mise en application

Pour exécuter les tests, nous utilisons la commande que nous connaissons maintenant :

cargo test

La commande se charge de tout et retourne :

   Compiling mon_api_maths v0.1.0 (/home/user/mesprojets/mon_api_maths)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running unittests src/lib.rs (target/debug/deps/mon_api_maths-da3e7a1947867a35)

running 1 test
test tests::test_limite_debordement ... ok

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

     Running unittests src/main.rs (target/debug/deps/mon_api_maths-44a383f15f2f56d8)

running 0 tests

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

   Doc-tests mon_api_maths

running 1 test
test src/lib.rs - calculer_perimetre (line 5) ... ok

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

all doctests ran in 0.23s; merged doctests compilation took 0.23s

Nos retrouvons :

  • notre nouveau test unitaire : test tests::test_limite_debordement ... ok et
  • le test de l’exemple de documentation : test src/lib.rs - calculer_perimetre (line 5) ... ok