Testes avançados em Rust: de unit a E2E
Testes são uma parte essencial do desenvolvimento de software de sistema confiável em Rust. Se os conceitos básicos (escrever testes simples com #[test]) são familiares a todos, as técnicas avançadas — trabalho com mocks, parametrização, atributos personalizados e organização de testes E2E — exigem uma análise separada. Neste artigo, veremos como elevar os testes em Rust a um nível profissional, usando abordagens unit, integration e end-to-end.
1. Testes unitários: mock, parametrização e atributos personalizados
Testes unitários em Rust geralmente são escritos no mesmo arquivo que o código, no módulo tests. No entanto, em um nível avançado, surge a necessidade de isolar o código testado de dependências externas.
Objetos mock com traits
Para criar mocks em Rust, geralmente se usam traits. Você define um trait, implementa-o para o objeto real e para o mock de teste. Crates populares: mockall e mockito (para HTTP).
use mockall::automock;
#[automock]trait Database { fn get_user(&self, id: u32) -> String;}
fn process_user(db: &dyn Database, id: u32) -> String { let user = db.get_user(id); format!("User: {}", user)}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_process_user() { let mut mock = MockDatabase::new(); mock.expect_get_user() .with(predicate::eq(42)) .returning(|_| "Alice".to_string()); assert_eq!(process_user(&mock, 42), "User: Alice"); }}Testes parametrizados
Repetir o mesmo teste com diferentes dados de entrada é uma tarefa comum. O crate rstest fornece uma solução elegante.
use rstest::rstest;
#[rstest]#[case(2, 3, 5)]#[case(0, 0, 0)]#[case(-1, 1, 0)]fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) { assert_eq!(a + b, expected);}Atributos de teste personalizados
Às vezes, é necessária lógica antes ou depois do teste. Você pode criar seus próprios atributos através de macros procedurais, mas é mais simples usar o crate test-case ou ctor para inicialização.
// Exemplo com ctor para inicialização do logger#[cfg(test)]mod tests { use ctor::ctor;
#[ctor] fn init() { // Inicialização única antes de todos os testes }}2. Testes de integração: organização e trabalho com dependências externas
Testes de integração em Rust estão localizados na pasta tests/ no nível raiz do projeto. Cada arquivo lá é um crate separado. Técnicas avançadas incluem módulos compartilhados e testes com bancos de dados reais ou APIs.
Utilitários compartilhados para testes de integração
Para evitar duplicação de código (por exemplo, configuração de servidor de teste), crie um módulo common dentro de tests/.
// tests/common/mod.rspub fn setup_test_db() -> String { // Configuração do banco de dados de teste "connected".to_string()}
// tests/integration_test.rsmod common;
#[test]fn test_db_connection() { let db = common::setup_test_db(); assert_eq!(db, "connected");}Trabalhando com requisições HTTP
Para testar clientes HTTP, use mockito — ele levanta um servidor mock local.
use mockito::{mock, server_url};
#[tokio::test]async fn test_api_call() { let _m = mock("GET", "/users/1") .with_status(200) .with_body(r#"{"id":1,"name":"Alice"}"#) .create();
let client = reqwest::Client::new(); let resp = client.get(&format!("{}/users/1", server_url())) .send() .await .unwrap(); assert!(resp.status().is_success());}3. Testes E2E: ciclo completo da aplicação
Testes end-to-end verificam o funcionamento do sistema como um todo: desde a inicialização