Tests avancés en Rust : des tests unitaires aux tests E2E
\nLes tests sont une partie essentielle du développement de logiciels système fiables en Rust. Si les concepts de base (écrire simplement des tests avec #[test]) sont familiers à tous, les techniques avancées — travail avec des mocks, paramétrisation, attributs personnalisés et organisation des tests E2E — nécessitent une analyse distincte. Dans cet article, nous verrons comment amener les tests en Rust à un niveau professionnel, en utilisant les approches unit, integration et end-to-end.
1. Tests unitaires : mock, paramétrisation et attributs personnalisés
\nLes tests unitaires en Rust sont généralement écrits dans le même fichier que le code, dans le module tests. Cependant, au niveau avancé, il devient nécessaire d'isoler le code testé des dépendances externes.
Objets mock avec des traits
\nPour créer des mocks en Rust, on utilise le plus souvent des traits. Vous définissez un trait, vous l'implémentez pour l'objet réel et pour le mock de test. Crates populaires : mockall et mockito (pour HTTP).
use mockall::automock;\n\n#[automock]\ntrait Database {\n fn get_user(&self, id: u32) -> String;\n}\n\nfn process_user(db: &dyn Database, id: u32) -> String {\n let user = db.get_user(id);\n format!("User: {}", user)\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_process_user() {\n let mut mock = MockDatabase::new();\n mock.expect_get_user()\n .with(predicate::eq(42))\n .returning(|_| "Alice".to_string());\n assert_eq!(process_user(&mock, 42), "User: Alice");\n }\n}\n\nTests paramétrés
\nRépéter le même test avec différentes données d'entrée est une tâche courante. Le crate rstest fournit une solution élégante.
use rstest::rstest;\n\n#[rstest]\n#[case(2, 3, 5)]\n#[case(0, 0, 0)]\n#[case(-1, 1, 0)]\nfn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {\n assert_eq!(a + b, expected);\n}\n\nAttributs de test personnalisés
\nParfois, une logique avant ou après le test est nécessaire. Vous pouvez créer vos propres attributs via des macros procédurales, mais il est plus simple d'utiliser le crate test-case ou ctor pour l'initialisation.
// Exemple avec ctor pour initialiser un logger\n#[cfg(test)]\nmod tests {\n use ctor::ctor;\n\n #[ctor]\n fn init() {\n // Initialisation une fois avant tous les tests\n }\n}\n\n2. Tests d'intégration : organisation et travail avec les dépendances externes
\nLes tests d'intégration en Rust se trouvent dans le dossier tests/ à la racine du projet. Chaque fichier y est un crate séparé. Les techniques avancées incluent des modules communs et des tests avec de vraies bases de données ou API.
Utilitaires communs pour les tests d'intégration
\nPour éviter de dupliquer le code (par exemple, la configuration d'un serveur de test), créez un module common dans tests/.
// tests/common/mod.rs\npub fn setup_test_db() -> String {\n // Configuration de la base de données de test\n "connected".to_string()\n}\n\n// tests/integration_test.rs\nmod common;\n\n#[test]\nfn test_db_connection() {\n let db = common::setup_test_db();\n assert_eq!(db, "connected");\n}\n\nTravail avec les requêtes HTTP
\nPour tester les clients HTTP, utilisez mockito — il lance un serveur mock local.
use mockito::{mock, server_url};\n\n#[tokio::test]\nasync fn test_api_call() {\n let _m = mock("GET", "/users/1")\n .with_status(200)\n .with_body(r#"{"id":1,"name":"Alice"}"#)\n .create();\n\n let client = reqwest::Client::new();\n let resp = client.get(&format!("{}/users/1", server_url()))\n .send()\n .await\n .unwrap();\n assert!(resp.status().is_success());\n}\n\n3. Tests E2E : cycle complet de l'application
\nLes tests end-to-end vérifient le fonctionnement de l'ensemble du système : du lance