ProgrammationDéveloppeur Rust / Testeur

Comment fonctionne le système de tests unitaires en Rust, et pourquoi les tests sont-ils étroitement intégrés au langage ? Comment organiser correctement les tests, garantir leur isolation et leur lisibilité ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Rust a été conçu dès le départ en mettant l'accent sur la fiabilité et la sécurité, ce qui fait des tests unitaires une partie intégrante de l'écosystème du langage. Les tests sont intégrés au niveau du langage et de la chaîne d'outils (cargo test), ce qui les rend une partie organique du processus de développement.

Historique de la question

Dans les écosystèmes traditionnels (comme C/C++, Python, Java), les tests existaient en dehors du programme lui-même. En Rust, les tests sont partie intégrante du code, ils sont compilés et vérifiés comme un module à part entière. Cette synergie est accomplie grâce aux constructions linguistiques et aux caractéristiques du compilateur.

Problème

Sans tests appropriés, il est impossible de garantir la fiabilité des fonctionnalités clés. Pour des projets complexes et multmodules, la question se pose souvent : comment organiser les tests de manière pratique, sans les rendre dépendants de l'état d'autres modules et sans compliquer la structure du projet ?

Solution

Dans Rust, les tests sont placés à l'intérieur du fichier source lui-même (à l'aide de #[cfg(test)]), ou dans un dossier séparé tests pour les tests d'intégration. Des tests privés peuvent être ajoutés à chaque module, ayant accès à l'API privée.

Exemple de code :

pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, -3), -4); } }

Caractéristiques clés :

  • Les tests sont compilés avec le code et ont accès aux fonctions privées du module.
  • Les tests d'intégration (dossier tests) simulent le fonctionnement de la bibliothèque en tant qu'entité externe.
  • Isolation des tests par processus : chaque test est exécuté indépendamment, et peut être parallélisé.

Questions pièges.

À quoi sert use super::*; dans le module de test ?

Pour que les tests puissent accéder aux fonctions et structures du module actuel (y compris privées), on utilise généralement use super::*; dans le test.

Un #[test] peut-il être asynchrone ?

Par défaut, #[test] ne prend pas en charge async, mais en utilisant des crate externes (comme tokio ou async-std), il est possible de faire un #[test] asynchrone.

Exemple de code :

#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }

Les tests peuvent-ils modifier l'état global et comment l'éviter ?

Les tests en Rust s'exécutent par défaut en parallèle, donc il est impossible d'utiliser un état global mutable partagé sans synchronisation (static mut), sinon des courses se produiront.

Erreurs typiques et anti-patterns

  • Utilisation de variables globales mutables dans les tests.
  • Séparation non évidente des tests entre module tests et tests d'intégration.
  • Absence de scénarios de test négatifs.

Exemple de la vie réelle

Cas négatif

Tous les tests utilisent une variable globale :

static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }

Avantages :

  • Facile à mettre en œuvre un compteur.

Inconvénients :

  • Conflits de données, comportement imprévisible.
  • Les tests peuvent accidentellement dépendre les uns des autres.

Cas positif

Chaque test est isolé, utilise des variables locales et des objets mock.

Avantages :

  • Pas d'effet d'interaction inattendue entre les tests.
  • L'exécution des tests en parallèle est sécurisée.

Inconvénients :

  • Doit être conçu avec plus de soin pour le code à tester.
  • Parfois, il faut plus de code pour initialiser l'état.