Tests en Rust

Tests en Rust

1. Tests unitaires

1.1 Syntaxe de base

pub fn ajouter(a: i32, b: i32) -> i32 {
    a + b
}

pub fn est_pair(n: i32) -> bool {
    n % 2 == 0
}

#[cfg(test)]
mod tests {
    use super::*;  // importe tout du module parent

    #[test]
    fn test_ajouter() {
        assert_eq!(ajouter(2, 3), 5);
    }

    #[test]
    fn test_est_pair() {
        assert!(est_pair(4));
        assert!(!est_pair(3));
    }
}

1.2 Macros d'assertion

// assert! — vérifie qu'une condition est vraie
assert!(2 + 2 == 4);
assert!(vec![1, 2, 3].len() == 3, "longueur incorrecte");

// assert_eq! — vérifie l'égalité (affiche les deux valeurs si échec)
assert_eq!(2 + 2, 4);
assert_eq!(
    "bonjour".to_uppercase(),
    "BONJOUR",
    "conversion en majuscules échouée"
);

// assert_ne! — vérifie l'inégalité
assert_ne!(2 + 2, 5);

// Pour les flottants — comparer avec une tolérance
let resultat = 0.1 + 0.2;
assert!((resultat - 0.3).abs() < 1e-10);

1.3 Tests qui doivent paniquer

pub fn diviser(a: i32, b: i32) -> i32 {
    if b == 0 { panic!("division par zéro"); }
    a / b
}

#[test]
#[should_panic]
fn test_division_par_zero() {
    diviser(10, 0);
}

// Vérifier le message de panique
#[test]
#[should_panic(expected = "division par zéro")]
fn test_message_panique() {
    diviser(10, 0);
}

1.4 Tests ignorés

#[test]
#[ignore = "trop lent pour le CI"]
fn test_lent() {
    // prend plusieurs minutes...
}
cargo test -- --ignored          # exécute seulement les tests ignorés
cargo test -- --include-ignored  # exécute tous les tests y compris ignorés

2. Organisation avec cfg(test)

// src/lib.rs

pub struct Calculatrice {
    valeur: f64,
}

impl Calculatrice {
    pub fn new(v: f64) -> Self { Calculatrice { valeur: v } }
    pub fn ajouter(&mut self, n: f64) -> &mut Self { self.valeur += n; self }
    pub fn resultat(&self) -> f64 { self.valeur }

    // Méthode privée — testable uniquement dans cfg(test)
    fn valeur_interne(&self) -> f64 { self.valeur }
}

#[cfg(test)]  // ce module n'est compilé que lors des tests
mod tests {
    use super::*;

    #[test]
    fn test_addition_chainee() {
        let mut calc = Calculatrice::new(0.0);
        calc.ajouter(5.0).ajouter(3.0);
        assert_eq!(calc.resultat(), 8.0);
    }

    #[test]
    fn test_valeur_interne() {
        let calc = Calculatrice::new(42.0);
        assert_eq!(calc.valeur_interne(), 42.0); // accès au privé OK dans tests
    }
}

3. Tests d'intégration

Les tests d'intégration testent l'API publique comme un utilisateur externe.

mon_projet/
├── src/
│   └── lib.rs
└── tests/              ← dossier des tests d'intégration
    ├── api_test.rs
    └── helpers/
        └── mod.rs
// tests/api_test.rs
use mon_projet::Calculatrice;  // uniquement l'API publique

#[test]
fn test_integration_calcul() {
    let mut calc = Calculatrice::new(10.0);
    calc.ajouter(5.0);
    assert_eq!(calc.resultat(), 15.0);
}
// tests/helpers/mod.rs — helpers partagés entre tests
pub fn creer_calculatrice_initialisee() -> mon_projet::Calculatrice {
    mon_projet::Calculatrice::new(100.0)
}
// tests/autre_test.rs
mod helpers;

#[test]
fn test_avec_helper() {
    let calc = helpers::creer_calculatrice_initialisee();
    assert_eq!(calc.resultat(), 100.0);
}

4. Tests de documentation

Les exemples dans la documentation sont exécutés comme des tests.

/// Additionne deux nombres.
///
/// # Exemples
///
/// ```
/// let resultat = mon_projet::ajouter(2, 3);
/// assert_eq!(resultat, 5);
/// ```
///
/// ```
/// // Un exemple qui doit paniquer
/// # use mon_projet::diviser;
/// let r = diviser(10, 2);
/// assert_eq!(r, 5);
/// ```
pub fn ajouter(a: i32, b: i32) -> i32 {
    a + b
}
cargo test --doc  # exécute uniquement les tests de documentation

Syntaxe spéciale dans les doc-tests :

/// ```
/// # // Les lignes préfixées par # sont exécutées mais pas affichées
/// # let x = 5;
/// println!("{x}"); // affiché dans la doc
/// # assert_eq!(x, 5);
/// ```

5. Commandes cargo test

cargo test                        # tous les tests
cargo test nom_du_test            # filtre par nom (sous-chaîne)
cargo test tests::                # tests dans le module 'tests'

cargo test -- --nocapture         # affiche println! dans les tests
cargo test -- --test-threads=1    # séquentiel (utile si tests partagent état)
cargo test -- --ignored           # tests marqués #[ignore]

cargo test --lib                  # tests unitaires uniquement (src/)
cargo test --test api_test        # un fichier d'intégration spécifique
cargo test --doc                  # tests de documentation uniquement

cargo test -p ma_crate            # dans un workspace, une crate spécifique

6. Bonnes pratiques

Nommer les tests clairement :

// ❌ Trop vague
#[test] fn test1() { }

// ✅ Nom descriptif
#[test] fn ajouter_deux_positifs_retourne_leur_somme() { }
#[test] fn diviser_par_zero_panique() { }

Arranger / Agir / Vérifier (AAA) :

#[test]
fn test_insertion_element() {
    // Arranger
    let mut liste = Vec::new();

    // Agir
    liste.push(42);

    // Vérifier
    assert_eq!(liste.len(), 1);
    assert_eq!(liste[0], 42);
}

Tests paramétrés avec une boucle :

#[test]
fn test_est_pair_plusieurs_valeurs() {
    let cas = [(0, true), (1, false), (2, true), (99, false), (100, true)];
    for (entree, attendu) in cas {
        assert_eq!(est_pair(entree), attendu, "est_pair({entree}) devrait être {attendu}");
    }
}