Introducción a las pruebas de integración de Rust con Docker
\nLas pruebas de integración son una etapa clave en el desarrollo de aplicaciones de servidor robustas en Rust. A diferencia de las pruebas unitarias, las pruebas de integración verifican la interacción de los componentes con dependencias reales: bases de datos, cachés, API externas. El uso de Docker permite aislar el entorno de prueba y garantizar la reproducibilidad. En este artículo, analizaremos técnicas avanzadas: ejecución paralela de contenedores, gestión de estado, servicios mock y trabajo con asincronía a través de Tokio.
\n\nConfiguración del entorno de prueba: Docker y testcontainers
\nPara las pruebas de integración en Rust, es conveniente usar la crate testcontainers. Permite iniciar contenedores Docker mediante programación, esperar a que estén listos y limpiar los recursos correctamente. Comencemos con la configuración básica.
Instalación de dependencias
\nAñada a 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"\nEjemplo básico: contenedor con PostgreSQL
\nCreemos una prueba que levante PostgreSQL, ejecute una migración y verifique la inserción de datos:
\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}\nEl contenedor se eliminará automáticamente después de que finalice la prueba gracias al destructor de Container. Esto evita la limpieza manual.
Ejecución paralela de pruebas y gestión de recursos
\nPor defecto, Rust ejecuta las pruebas en hilos paralelos. Si cada prueba levanta su propio contenedor, esto puede provocar conflictos de puertos y sobrecargar Docker. Consideremos dos enfoques.
\nUso de diferentes puertos mediante asignación dinámica
\nLa crate testcontainers asigna automáticamente puertos libres en el host. Esto resuelve el problema de colisiones, pero aumenta el consumo de recursos. Para contenedores pesados (por ejemplo, bases de datos), es mejor usar una instancia compartida:
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 // ... conexión como en el ejemplo anterior\n (client, container)\n}\nSin embargo, este enfoque es peligroso: las pruebas pueden afectarse entre sí a través del estado compartido. Se recomienda usar bases de datos separadas dentro de un mismo contenedor o restablecer el estado