Einführung in Integrationstests von Rust mit Docker
\nIntegrationstests sind ein entscheidender Schritt bei der Entwicklung zuverlässiger Serveranwendungen in Rust. Im Gegensatz zu Unit-Tests überprüfen Integrationstests die Interaktion von Komponenten mit realen Abhängigkeiten: Datenbanken, Caches, externen APIs. Die Verwendung von Docker ermöglicht es, die Testumgebung zu isolieren und Reproduzierbarkeit zu gewährleisten. In diesem Artikel behandeln wir fortgeschrittene Techniken: paralleles Starten von Containern, Zustandsverwaltung, Mock-Dienste und die Arbeit mit Asynchronität über Tokio.
\n\nEinrichtung der Testumgebung: Docker und testcontainers
\nFür Integrationstests in Rust ist die Crate testcontainers praktisch. Sie ermöglicht das programmatische Starten von Docker-Containern, das Warten auf deren Bereitschaft und die korrekte Bereinigung von Ressourcen. Beginnen wir mit der grundlegenden Einrichtung.
Abhängigkeiten installieren
\nFügen Sie in Cargo.toml hinzu:
[dev-dependencies]\ntestcontainers = "0.21"\ntokio = { version = "1", features = ["full"] }\nreqwest = { version = "0.12", features = ["json"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\nBasisbeispiel: Container mit PostgreSQL
\nErstellen wir einen Test, der PostgreSQL startet, eine Migration durchführt und das Einfügen von Daten überprüft:
\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("Failed to connect");\n tokio::spawn(async move {\n if let Err(e) = connection.await {\n eprintln!("Connection error: {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}\nDer Container wird nach Abschluss des Tests automatisch gelöscht, dank des Destruktors von Container. Dies erspart die manuelle Bereinigung.
Paralleles Ausführen von Tests und Ressourcenverwaltung
\nStandardmäßig führt Rust Tests in parallelen Threads aus. Wenn jeder Test seinen eigenen Container startet, kann dies zu Portkonflikten und einer Überlastung von Docker führen. Betrachten wir zwei Ansätze.
\nVerwendung verschiedener Ports durch dynamische Zuweisung
\nDie Crate testcontainers weist automatisch freie Ports auf dem Host zu. Dies löst das Problem von Kollisionen, erhöht jedoch den Ressourcenverbrauch. Für schwere Container (z. B. Datenbanken) ist es besser, eine gemeinsame Instanz zu verwenden:
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 // ... Verbindung wie im vorherigen Beispiel\n (client, container)\n}\nDieser Ansatz ist jedoch riskant: Tests können sich gegenseitig über den gemeinsamen Zustand beeinflussen. Es wird empfohlen, separate Datenbanken innerhalb eines Containers zu verwenden oder den Zustand zurückzusetzen.