Le paradoxe des 100% de couverture — ou pourquoi vos tests peuvent vous mentir
\n\nVous avez écrit un code Rust parfait. Le compilateur est content, clippy ne dit rien, les tests passent au vert, et le rapport de couverture annonce fièrement : 100% des lignes sont couvertes. Vous pouvez souffler ? Non. Le mutation testing en Rust est précisément la technique qui vous montrera que vos tests protègent peut-être du vent, et non la logique.
\n\nImaginez : vous changez accidentellement un + en - dans une fonction critique. Les tests sont toujours verts ? Félicitations, vous avez trouvé un mutant — un défaut qui vit dans votre code mais que vos tests ignorent. Dans cet article, nous allons voir comment le test de mutation en Rust vous aide à repérer ces angles morts, et nous l’intégrerons dans un projet réel.
Qu’est-ce que le mutation testing (et pourquoi Rust est idéal)
\n\nLe mutation testing (ou test de mutation) est une méthode d’évaluation de la qualité des tests qui consiste à introduire de petites modifications artificielles (mutations) dans le code source, puis à exécuter les tests. Si les tests ne détectent pas le code muté, le mutant a survécu. Plus il y a de mutants survivants, moins vos tests sont efficaces.
\n\nRust est un langage qui, à lui seul, détecte des classes entières d’erreurs à la compilation : conditions de concurrence, pointeurs nuls, use-after-free. Mais le mutation testing en Rust cible spécifiquement les erreurs logiques — ce que le compilateur ne voit pas. Cela fait de Rust un terrain de jeu idéal : vous êtes déjà protégé contre 70% des bugs classiques, et le test de mutation achève les 30% restants.
\n\nÀ quoi ressemble une mutation dans du code Rust
\n\nLes mutations typiques incluent :
\n- \n
- Changement d’opérateurs :
a + b→a - b\n - Inversion de conditions :
if x > 5→if x < 5\n - Remplacement de constantes :
let n = 42→let n = 0\n - Suppression d’appels de fonctions :
process(data)→ appel vide \n - Modification des bornes d’itération :
0..n→0..=n\n
Si après une telle mutation au moins un test ne tombe pas — alors vous avez perdu votre temps à écrire ce test. Il ne vérifie pas ce qu’il devrait.
\n\nOutils pour le mutation testing en Rust : cargo-mutants
\n\nÀ ce jour, l’outil principal pour le test de mutation en Rust est cargo-mutants. Il est mature, activement maintenu et fournit des rapports lisibles. L’installation est simple :
cargo install cargo-mutants\n\n\nEnsuite, il suffit de lancer :
\n\ncargo mutants\n\n\nL’outil trouve automatiquement toutes les mutations dans votre code, exécute les tests et génère un rapport HTML avec un code couleur : vert pour les mutants tués (les tests ont fonctionné), rouge pour les survivants (problème).
\n\nAlternatives à surveiller
\n- \n
- mutagen — plus ancien, mais toujours fonctionnel. Moins de mutations, plus rapide sur les petits projets. \n
- cargo-insta — pas tout à fait du mutation testing, mais proche : il aide à vérifier visuellement les changements dans les snapshots de test. \n
Pour un travail sérieux, je recommande cargo-mutants — il offre le meilleur équilibre entre vitesse et profondeur d’analyse.
Exemple pratique : testons un calculateur de réductions
\n\nSupposons que nous ayons un module pour calculer des réductions dans une boutique en ligne, écrit en Rust :
\n\n// src/discount.rs\n\npub fn calculate_discount(price: f64, customer_age: u32) -> f64 {\n let mut discount = 0.0;\n\n // 10% de réduction pour les seniors (plus de 65 ans)\n if customer_age > 65 {\n discount += 0.1;\n }\n\n // 5% de réduction supplémentaire pour les achats de plus de 1000 euros\n if price > 1000.0 {\n discount += 0.05;\n }\n\n // Réduction maximale de 30%\n if discount > 0.30 {\n discount = 0.30;\n