Introduction aux tests d'intégration Rust avec Docker
\nLes tests d'intégration sont une étape clé du développement d'applications serveur fiables en Rust. Contrairement aux tests unitaires, les tests d'intégration vérifient l'interaction des composants avec des dépendances réelles : bases de données, caches, API externes. L'utilisation de Docker permet d'isoler l'environnement de test et de garantir la reproductibilité. Dans cet article, nous aborderons des techniques avancées : exécution parallèle de conteneurs, gestion d'état, services mock et travail avec l'asynchronisme via Tokio.
\n\nConfiguration de l'environnement de test : Docker et testcontainers
\nPour les tests d'intégration en Rust, il est pratique d'utiliser la crate testcontainers. Elle permet de lancer des conteneurs Docker par programmation, d'attendre qu'ils soient prêts et de nettoyer correctement les ressources. Commençons par la configuration de base.
Installation des dépendances
\nAjoutez dans Cargo.toml :
[dev-dependencies]\ntestcontainers = "0.21"\ntokio = { version = "1", features = ["full"] }\nreqwest = { version = "0.12", features = ["json"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\nExemple de base : conteneur avec PostgreSQL
\nCréons un test qui démarre PostgreSQL, exécute une migration et vérifie l'insertion de données :
\nuse testcontainers::{clients, images::postgres::Postgres, Container};\nuse tokio_postgres::{NoTls, Client};\n\nasync fn setup_db() -> (Client, Container<'static, Postgres>) {\n let docker = clients::Cli::default();\n let pg_image = Postgres::default();\n let container = docker.run(pg_image);\n let host = container.get_host();\n let port = container.get_host_port_ipv4(5432);\n let connection_string = format!(\n "host={host} port={port} user=postgres dbname=postgres",\n );\n let (client, connection) = tokio_postgres::connect(&connection_string, NoTls)\n .await\n .expect("Échec de la connexion");\n tokio::spawn(async move {\n if let Err(e) = connection.await {\n eprintln!("Erreur de connexion : {e}");\n }\n });\n (client, container)\n}\n\n#[tokio::test]\nasync fn test_insert_user() {\n let (client, _container) = setup_db().await;\n client\n .simple_query("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)")\n .await\n .unwrap();\n client\n .execute("INSERT INTO users (name) VALUES ($1)", &[&"Alice"])\n .await\n .unwrap();\n let rows = client.query("SELECT name FROM users", &[]).await.unwrap();\n assert_eq!(rows.len(), 1);\n assert_eq!(rows[0].get::<_, String>(0), "Alice");\n}\nLe conteneur sera automatiquement supprimé après la fin du test grâce au destructeur de Container. Cela évite un nettoyage manuel.
Exécution parallèle des tests et gestion des ressources
\nPar défaut, Rust exécute les tests dans des threads parallèles. Si chaque test démarre son propre conteneur, cela peut entraîner des conflits de ports et une surcharge de Docker. Examinons deux approches.
\nUtilisation de ports différents via l'allocation dynamique
\nLa crate testcontainers attribue automatiquement des ports libres sur l'hôte. Cela résout le problème de collisions, mais augmente la consommation de ressources. Pour les conteneurs lourds (par exemple, les bases de données), il est préférable d'utiliser une instance partagée :
use once_cell::sync::Lazy;\nuse testcontainers::clients;\n\nstatic DOCKER: Lazy<clients::Cli> = Lazy::new(|| clients::Cli::default());\n\nasync fn get_shared_postgres() -> (tokio_postgres::Client, Container<'static, Postgres>) {\n let pg_image = Postgres::default();\n let container = DOCKER.run(pg_image);\n // ... connexion comme dans l'exemple précédent\n (client, container)\n}\nCependant, cette approche est risquée : les tests peuvent s'influencer mutuellement via un état partagé. Il est recommandé d'utiliser des bases de données distinctes dans un même conteneur ou de réinitialiser l'état