Cours de programmation séquentielle

Tests automatiques

Orestis Malaspinas

Tests automatiques

A quoi ça sert?

  • Nous permet d’avoir confiance en nos programmes.
  • Tester que notre programme se comporte comme désiré.
  • Le compilateur ne peut pas détecter tous les bugs possibles: les erreurs algorithmiques ne peuvent pas être détectées.
  • Éviter les régressions lorsqu’on réécrit ou étend les fonctionalités d’un programme.
  • Néanmois: impossible de tester si un programme est complètement correct (on ne peut pas tester tous les cas).
  • Plus de détails et d’informations: ici

Comment écrire un test

En général un test effectue les trois actions suivantes:

  1. Mettre en place les données ou un état donné.
  2. Exécuter le code à tester.
  3. Vérifier si le résultat est celui attendu.

Exemple

  • Il est très aisé de voir l’anatomie d’un test très simple:
    
    cargo new projet_test --lib
    
  • Création d’un projet avec le fichier projet_test/src/lib.rs
    
      #[cfg(test)] // cette partie sera exécuté que lors du test
    mod tests {
      #[test] // la fonction suivant cette annotation sera exécutée comme test
      fn it_works() {
          assert_eq!(2 + 2, 4); // Vérification que l'addition fonctionne pour 2+2
          assert_ne!(2 + 2, 6); // utilisation de assert_ne!
          assert!(2 + 2 == 4); // utilisation de assert! 2+2
      }
    }
    
  • Le test est exécuté avec cargo test
    
     Compiling projet_test v0.1.0 (projet_test)
      Finished dev [unoptimized + debuginfo] target(s) in 0.45s
       Running projet_test/target/debug/deps/projet_test-05539f5503f2a0fe\
    \
    running 1 test
    test tests::it_works ... ok\
    \
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\
    \
     Doc-tests projet_test\
    \
    running 0 tests\
    \
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
    

Tests de fonctions

  • Le test d’une fonction nécessite de la mettre dans la porté

    fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*; // mise en portée de tout ce qui est dans le module de dessus

    #[test]
    fn test_add_two() {
        assert_eq!(add_two(2), 4); // Vérification de la fonction add_two
    }

    #[test]
    fn test_add_two_custom_msg() {
        // Ce test va paniquer à dessein, pour voir le message personnalisé. 
        assert_eq!(add_two(2) == 5, "Attention la somme 2+2 ne donne pas 5 mais {}.", 4);
    }
}

Tests devant paniquer

  • Si une erreur de logique est suffisamment grave pour faire paniquer le code on peut la panique:

    fn two_div_by(a: i32) -> i32 {
    if a == 0 {
        panic!("On nee peut pas diviser par zéro.");
    } else {
        return 2 / a;
    }
}

#[cfg(test)]
mod tests {
    use super::*; // mise en portée de tout ce qui est dans le module de dessus

    #[test]
    #[should_panic] // ce test doit paniquer, s'il panique pas il est raté
    fn two_div_fail() {
        two_div_by(0);
    }
}

Affichage

  • Les tests n’affichent rien si le test est réussi (la sortie standard est capturée).
  • Si le test est raté, le message d’erreur et tout ce qui aurait dû être affiché à l’écran.
  • Les tests sont effectués en parallèle pour des question d’efficacité:
    • L’affichage à l’écran peut s’en retrouver perturbé (affichage dans un ordre erratique).
    • Pour les exécuter séquentiellement: cargo test --test-threads=1.
  • Pour afficher tout ce qui devrait être affiché à l’écran dans le bon ordre (même les tests réussis):
    • cargo test -- --nocapture --test-threads=1

Fonctionalités supplémentaires (voir le livre)

  • Tests d’intégration:
    • On peut grouper les tests par fonctions.
    • On peut grouper les tests par sous-modules.
    • On peut tester les sous-modules indépendamment.