Introdução aos Testes de Integração Rust com Docker
\nTestes de integração são uma etapa fundamental no desenvolvimento de aplicações servidoras confiáveis em Rust. Diferentemente dos testes unitários, os testes de integração verificam a interação dos componentes com dependências reais: bancos de dados, caches, APIs externas. O uso do Docker permite isolar o ambiente de teste e garantir a reprodutibilidade. Neste artigo, abordaremos técnicas avançadas: execução paralela de contêineres, gerenciamento de estado, serviços mock e trabalho com assincronicidade via Tokio.
\n\nConfiguração do Ambiente de Teste: Docker e testcontainers
\nPara testes de integração em Rust, é conveniente usar a crate testcontainers. Ela permite iniciar contêineres Docker programaticamente, aguardar sua prontidão e limpar recursos corretamente. Vamos começar com a configuração básica.
Instalação de Dependências
\nAdicione ao 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"\nExemplo Básico: Contêiner com PostgreSQL
\nVamos criar um teste que inicia o PostgreSQL, executa uma migração e verifica a inserção de dados:
\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("Falha ao conectar");\n tokio::spawn(async move {\n if let Err(e) = connection.await {\n eprintln!("Erro de conexão: {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}\nO contêiner será removido automaticamente após a conclusão do teste graças ao destrutor Container. Isso elimina a necessidade de limpeza manual.
Execução Paralela de Testes e Gerenciamento de Recursos
\nPor padrão, o Rust executa os testes em threads paralelas. Se cada teste iniciar seu próprio contêiner, isso pode causar conflitos de porta e sobrecarga do Docker. Vamos considerar duas abordagens.
\nUso de Portas Diferentes via Alocação Dinâmica
\nA crate testcontainers atribui automaticamente portas livres no host. Isso resolve o problema de colisões, mas aumenta o consumo de recursos. Para contêineres pesados (por exemplo, bancos de dados), é melhor usar uma instância compartilhada:
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 // ... conexão como no exemplo anterior\n (client, container)\n}\nNo entanto, essa abordagem é arriscada: os testes podem influenciar uns aos outros através do estado compartilhado. Recomenda-se usar bancos de dados separados dentro de um mesmo contêiner ou redefinir o estado