Pruebas de integración de Rust con Docker: de la teoría a la práctica
El desarrollo moderno en Rust requiere no solo verificar funciones individuales (pruebas unitarias), sino también probar la interacción de componentes con servicios externos — bases de datos, cachés, APIs. Las pruebas de integración con Docker permiten aislar el entorno, garantizar la reproducibilidad y evitar problemas de "funciona en mi máquina". En este artículo analizaremos cómo configurar pruebas de integración para aplicaciones Rust utilizando Docker.
¿Por qué Docker para pruebas de integración de Rust?
Docker resuelve tres problemas clave:
- Aislamiento del entorno — las pruebas se ejecutan en un contenedor limpio, sin influencia de configuraciones locales
- Reproducibilidad — mismas condiciones en todas las máquinas de desarrolladores y en CI/CD
- Gestión de dependencias — inicio automático de PostgreSQL, Redis, Kafka y otros servicios
Configuración del proyecto: estructura y dependencias
Crearemos un proyecto con pruebas de integración para un servidor web en Actix-web que funciona con PostgreSQL.
# Cargo.toml[package]name = "rust-docker-testing"version = "0.1.0"edition = "2021"
[dependencies]actix-web = "4"serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }dotenv = "0.15"
[dev-dependencies]testcontainers = "0.14"reqwest = { version = "0.11", features = ["json"] }Observa testcontainers — es una biblioteca de Rust para gestionar contenedores Docker directamente desde las pruebas.
Estructura del proyecto
Estructura recomendada para pruebas de integración:
.├── src/│ ├── main.rs│ ├── lib.rs│ ├── models.rs│ └── handlers.rs├── tests/│ ├── common/│ │ └── mod.rs # Utilidades comunes para pruebas│ ├── integration_tests.rs # Pruebas de integración principales│ └── docker-compose.yml # Para ejecución manual├── Dockerfile└── .envPaso 1: Escribir una prueba de integración con Docker
Crearemos una prueba que levante PostgreSQL en Docker, ejecute migraciones y verifique la API.
// tests/integration_tests.rsuse testcontainers::clients;use testcontainers::images::postgres::Postgres;use sqlx::postgres::PgPoolOptions;use std::net::TcpListener;
async fn setup_test_db() -> (String, testcontainers::Container) { let docker = clients::Cli::default(); let postgres_image = Postgres::default(); let container = docker.run(postgres_image); let host = container.get_host().unwrap(); let port = container.get_host_port_ipv4(5432).unwrap(); let database_url = format!("postgres://postgres:postgres@{}:{}/postgres", host, port); // Ejecutamos migraciones let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Error al conectar a Postgres"); sqlx::query("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)") .execute(&pool) .await .expect("Migración fallida"); (database_url, container)}
#[tokio::test]async fn test_create_user() { let (db_url, _container) = setup_test_db().await; // Iniciamos la aplicación en un puerto aleatorio let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); let server = actix_web::HttpServer::new(move || { actix_web::App::new() .app_data(actix_web::web::Data::new(db_url.clone())) .route("/users", actix_web::web::post().to(handlers::create_user)) }) .listen(listener) .unwrap() .run(); tokio::spawn(server); // Esperamos el inicio del servidor tokio::ti