Tests en Rust : des tests unitaires au property-based testing
Rust est l'un des rares langages où le système de types et de possession élimine dès la compilation des classes entières d'erreurs (conditions de concurrence, pointeurs pendants, double libération de mémoire). Cependant, les erreurs logiques, le traitement incorrect des cas limites et la logique métier erronée subsistent. Pour les détecter, l'écosystème Rust dispose d'un ensemble puissant d'outils de test, du test-runner intégré aux frameworks avancés de génération de données de test.
Outils intégrés : cargo test
L'outil de test de base est intégré directement dans le compilateur et le gestionnaire de paquets. Pour créer des tests, il suffit d'ajouter un module avec l'attribut #[cfg(test)] et de marquer les fonctions avec l'attribut #[test].
// src/lib.rspub fn add(left: u64, right: u64) -> u64 { left + right}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_add() { let result = add(2, 2); assert_eq!(result, 4); }
#[test] fn test_add_negative() { // u64 ne supporte pas les nombres négatifs — c'est une erreur de compilation // Mais si la fonction acceptait i64 : // assert_eq!(add(-2, 2), 0); }}Exécution : cargo test. Le flag -- --nocapture affiche les println! des tests. Le flag -- --test-threads=1 désactive l'exécution parallèle.
Tests d'intégration
Les tests d'intégration se trouvent dans le dossier tests/ à la racine du projet. Chaque fichier est une crate séparée qui peut tester l'API publique de la bibliothèque.
// tests/integration_test.rsuse my_library::add;
#[test]fn test_integration() { assert_eq!(add(10, 20), 30);}Pour tester les crates binaires (avec main.rs), on peut utiliser assert_cmd — il exécute le binaire comme un processus externe et vérifie stdout/stderr.
Property-based testing : proptest et quickcheck
Au lieu d'écrire manuellement des dizaines de cas de test, le property-based testing génère des milliers de données d'entrée aléatoires et vérifie les invariants. Deux frameworks principaux :
- proptest — moderne, flexible, avec support des stratégies, du filtrage et de la réduction d'exemples (shrink).
- quickcheck — plus ancien, inspiré de Haskell QuickCheck, plus simple mais moins puissant.
Exemple avec proptest :
use proptest::prelude::*;
fn reverse_string(s: &str) -> String { s.chars().rev().collect()}
proptest! { #[test] fn test_reverse_twice(s in ".*") { let reversed = reverse_string(&s); let double_reversed = reverse_string(&reversed); assert_eq!(s, double_reversed); }
#[test] fn test_reverse_length(s in ".*") { let reversed = reverse_string(&s); assert_eq!(s.len(), reversed.len()); }}Si un test échoue, proptest trouve le contre-exemple minimal (shrinking).
Test de code asynchrone : tokio-test
Pour les fonctions asynchrones fonctionnant avec tokio, les tests classiques ne conviennent pas — un runtime asynchrone est nécessaire. tokio-test fournit la macro #[tokio::test].
use tokio::time::{sleep, Duration};
async fn delayed_add(a: u32, b: u32) -> u32 { sleep(Duration::from_millis(10)).await; a + b}
#[tokio::test]async fn test_delayed_add() { let result = delayed_add(3, 5).await; assert_eq!(result, 8);}On peut également utiliser futures-test pour travailler avec futures et async-std.
Mocks et stubs : mockall et mockito
Pour isoler les tests des dépendances externes (BDD, API, système de fichiers