Rust

Any donation is very welcome
Fork me on GitHub

III. Aller plus loin

3. Ajouter des tests

Ce chapitre parlera des tests et de la métadonnée #[test].
En Rust, il est possible d'écrire des tests unitaires directement dans un fichier qui peuvent être lancés par Cargo ou le compilateur de Rust.

Avec Cargo :

> cargo test

Avec rustc :

> rustc --test votre_fichier_principal.rs
> ./votre_fichier_principal

Regardons maintenant comment créer ces tests unitaires :

La métadonnée #[test]

Pour indiquer au compilateur que cette fonction est un test unique, il faut ajouter #[test]. Exemple :

Runfn some_func(valeur1: i32, valeur2: i32) -> i32 {
    valeur1 + valeur2
}

#[test]
fn test_some_func() {
    assert_eq!(3, some_func(1, 2));
}

Plutôt facile, non ? Vous pouvez aussi mettre cette balise sur un module :

Runfn some_func(valeur1: i32, valeur2: i32) -> i32 {
    valeur1 + valeur2
}

#[cfg(test)] // on ne compile que si on est en mode "test"
mod tests {
    use super::some_func;

    #[test] // on doit le remettre pour bien spécifier au compilateur que c'est un test
    fn test_some_func() {
        assert_eq!(3, some_func(1, 2));
    }
}

Ça permet de découper un peu le code.

La métadonnée #[should_panic]

Maintenant, si vous voulez vérifier qu'un test échoue, il vous faudra utiliser cette balise :

Runfn some_func(valeur1: i32, valeur2: i32) -> i32 {
    valeur1 + valeur2
}

#[test] // c'est un test
#[should_panic] // il est censé paniquer
fn test_some_func() {
    assert_eq!(4, some_func(1, 2)); // 1 + 2 != 4, donc ça doit paniquer
}

Quand vous lancerez l'exécutable, il vous confirmera que le test s'est bien déroulé (parce qu'il a paniqué). Petit bonus : vous pouvez ajouter du texte qui sera affiché lors de l'exécution du test :

Run#[test]
#[should_panic(expected = "1 + 2 != 4")]
fn test_some_func() {
    assert_eq!(4, some_func(1, 2));
}

Mettre les tests dans un dossier à part

Si vous utilisez Cargo, il est aussi possible d'écrire des tests dans un dossiers à part. Commencez par créer un dossier tests puis créez un fichier .rs.

Dans ce fichier, il vous faudra importer votre bibliothèque pour pouvoir tester ses fonctions :

Runextern crate ma_lib;

#[test]
fn test_some_func() {
    assert_eq!(3, ma_lib::some_func(1, 2));
}

Écrire des suites de tests

Si vous shouhaitez regrouper plusieurs tests dans un même dossier (mais toujours dans le dossier tests), rien de bien difficile une fois encore. Ça devra ressembler à ça :

 - tests
    |
    |- la_suite_de_tests.rs
    |- sous_dossier
        |
        |- fichier1.rs
        |- fichier2.rs
        |- mod.rs

Je pense que vous voyez déjà où je veux en venir : il va juste falloir importer le module sous_dossier pour que les tests contenus dans fichier1.rs et fichier2.rs soient exécutés.

la_suite_de_tests.rs
Runmod sous_dossier; // Et c'est tout !
sous_dossier/mod.rs
Runmod fichier1;
mod fichier2;

Et voilà ! Vous pouvez maintenant écrire tous les tests que vous voulez dans fichier1.rs et fichier2.rs (en n'oubliant pas d'ajouter #[test] !).

Tests dans la documentation ?

Comme vous le savez déjà, on peut ajouter des exemples de code dans la documentation. Ce que je ne vous avais pas dit, c'est que lorsque vous lancez cargo test, ces exemples sont eux aussi testés. C'est très pratique car cela permet de les maintenir à jour assez facilement.

Options de test

/// ```
/// let x = 12;
/// ```

C'est l'exemple de code par défaut. Si aucune option n'est passée, rustdoc partira donc du principe que c'est un code Rust et qu'il est censé compiler.

Il est strictement équivalent au code suivant :

/// ```rust
/// let x = 12;
/// ```

Si vous voulez écrire du code dans un autre langage, écrivez juste son nom à la place rust :

/// ```C
/// int c = 12;
/// ```

Dans ce cas-là, ce code sera ignoré lors des tests.

Il se peut aussi que vous ayez envie d'ignorer un test :

/// ```ignore
/// let x = 12;
/// ```

Il sera marqué comme ignored mais vous le verrez lors des tests.

Un autre cas assez courant est de vouloir tester que la compilation se passe bien mais sans exécuter le code (généralement pour des exemples d'I/O) :

/// ```no_run
/// let x = File::open("Un-fichier.txt").expect("Fichier introuvable");
/// ```

Il est aussi possible de combiner plusieurs options en les séparant par une virgule :

/// ```compile_fail,no_run
/// let x = 12;
/// ```

C'est généralement inutile mais sait-on jamais...

Une dernière option pour la route (un peu étrange) :

```test_harness
#[test]
fn foo() {
    fail!("oops! (will run & register as failure)")
}
```

Cela compile le code comme si le flag "--test" était donné au compilateur.

En bref, il y a pas mal d'options qui vous sont proposées dont voici la liste complète :

  • rust : par défaut
  • ignore : pour dire à rustdoc d'ignorer ce code
  • should_panic : le test échouera si le code s'exécute sans erreur
  • no_run : ne teste que la compilation
  • test_harness : compile comme si le flag "--test" était donné au compilateur
  • compile_fail : teste que la compilation échoue
  • allow_fail : en gros, si l'exécution échoue, ça ne fera pas échouer le test

Tout autre option sera considérée comme un langage et passera le code en ignore invisible (vous ne le verrez pas apparaitre dans la liste des codes testés).

Cacher des lignes

Pour certaines, vous pourriez vouloir cacher des lignes lors du rendu du code mais les gardez lors du test. Exemple :

/// ```
/// # fn foo() -> io::Result<()> {
/// let f = File::open("un-fichier.txt")?;
/// # }
/// ```

Quand la doc sera générée, le lecteur ne verra plus que :

Runlet f = File::open("un-fichier.txt")?;

Par-contre, lors du lancement des tests, tout le code sera bien présent. Plutôt pratique si jamais vous avez besoin de concentrer l'attention du lecteur sur un point précis !