Tests d'intégration Rust avec Docker : niveau avancé
Les tests d'intégration sont une étape cruciale du développement en Rust, surtout lorsque votre application interagit avec des services externes : bases de données, files d'attente de messages, API HTTP. L'utilisation de Docker permet de créer des environnements de test isolés et reproductibles. Dans cet article, nous aborderons des techniques avancées : de la configuration de base à l'exécution parallèle des tests avec testcontainers et docker-compose.
Pourquoi Docker pour les tests d'intégration ?
Les mocks et stubs traditionnels ne reflètent souvent pas le comportement réel des dépendances. Docker résout ce problème :
- Isolation : chaque test s'exécute dans un conteneur propre.
- Reproductibilité : environnement identique sur toutes les machines des développeurs et dans le CI.
- Parallélisme : possibilité d'exécuter plusieurs bases de données de test simultanément.
1. Configuration de base : test avec PostgreSQL
Commençons simplement : testons une fonction qui interagit avec PostgreSQL. Utilisons la crate testcontainers — un wrapper Rust pour gérer les conteneurs Docker.
Ajoutez les dépendances dans Cargo.toml :
[dev-dependencies]testcontainers = "0.18"tokio = { version = "1", features = ["full"] }sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }Exemple de test :
use sqlx::PgPool;use testcontainers::clients::Cli;use testcontainers::images::postgres::Postgres;
#[tokio::test]async fn test_database_connection() { let docker = Cli::default(); let postgres_instance = docker.run(Postgres::default()); let connection_string = &format!( "postgres://postgres:postgres@127.0.0.1:{}/postgres", postgres_instance.get_host_port_ipv4(5432) );
let pool = PgPool::connect(connection_string).await.unwrap();
// Exécution d'une requête SQL let row: (i32,) = sqlx::query_as("SELECT 1 + 1") .fetch_one(&pool) .await .unwrap(); assert_eq!(row.0, 2);}Important : le conteneur est automatiquement supprimé après la fin du test (grâce à l'implémentation de Drop).
2. Configuration avancée : image personnalisée avec migrations
Il est souvent nécessaire d'exécuter des migrations au préalable. Créons une image personnalisée basée sur un Dockerfile :
# Dockerfile.testFROM postgres:16-alpineCOPY migrations/ /docker-entrypoint-initdb.d/Dans le test Rust, utilisons Image :
use testcontainers::images::generic::GenericImage;
#[tokio::test]async fn test_with_migrations() { let docker = Cli::default(); let image = GenericImage::new("my-app-test:latest") .with_wait_for(testcontainers::core::WaitFor::message_on_stdout( "database system is ready to accept connections" )); let instance = docker.run(image); // ... utilisation de instance}Pour construire l'image dans le CI, vous pouvez utiliser docker build -f Dockerfile.test -t my-app-test:latest .
3. Exécution parallèle des tests avec Docker Compose
Si votre service nécessite plusieurs dépendances (PostgreSQL + Redis), utilisez docker-compose. Créez un fichier docker-compose.test.yml :
version: '3.8'services: postgres: image: postgres:16-alpine environment: POSTGRES_PASSWORD: testpass ports: - "5432" redis: image: redis:7-alpine ports: - "6379"Dans le test Rust, lancez l'environnement Compose :
use testcontainers::clients::Cli;use testcontainers::images::compose::Compose;
#[tokio::test]async fn test_with_compose() { let docker = Cli::default(); let compose = Compose::new("docker-compose.test.y