Pruebas avanzadas en Rust: desde unit hasta E2E
Las pruebas son una parte esencial del desarrollo de software de sistema confiable en Rust. Si los conceptos básicos (escribir pruebas simples con #[test]) son familiares para todos, las técnicas avanzadas —trabajar con mocks, parametrización, atributos personalizados y la organización de pruebas E2E— requieren un análisis aparte. En este artículo, veremos cómo llevar las pruebas en Rust a un nivel profesional, utilizando enfoques unit, integration y end-to-end.
1. Pruebas unitarias: mock, parametrización y atributos personalizados
Las pruebas unitarias en Rust generalmente se escriben en el mismo archivo que el código, en el módulo tests. Sin embargo, a nivel avanzado surge la necesidad de aislar el código bajo prueba de las dependencias externas.
Objetos mock con traits
Para crear mocks en Rust, se suelen usar traits. Usted define un trait, lo implementa para el objeto real y para el mock de prueba. Crates populares: mockall y 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"); }}Pruebas parametrizadas
Repetir la misma prueba con diferentes datos de entrada es una tarea común. El crate rstest proporciona una solución 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 prueba personalizados
A veces se requiere lógica antes o después de la prueba. Se pueden crear atributos propios mediante macros procedurales, pero es más sencillo usar el crate test-case o ctor para la inicialización.
// Ejemplo con ctor para inicializar un logger#[cfg(test)]mod tests { use ctor::ctor;
#[ctor] fn init() { // Inicialización única antes de todas las pruebas }}2. Pruebas de integración: organización y trabajo con dependencias externas
Las pruebas de integración en Rust se encuentran en la carpeta tests/ a nivel de la raíz del proyecto. Cada archivo allí es un crate independiente. Las técnicas avanzadas incluyen módulos compartidos y pruebas con bases de datos o API reales.
Utilidades compartidas para pruebas de integración
Para no duplicar código (por ejemplo, la configuración de un servidor de prueba), cree un módulo common dentro de tests/.
// tests/common/mod.rspub fn setup_test_db() -> String { // Configuración de la BD de prueba "connected".to_string()}
// tests/integration_test.rsmod common;
#[test]fn test_db_connection() { let db = common::setup_test_db(); assert_eq!(db, "connected");}Trabajo con solicitudes HTTP
Para probar clientes HTTP, use mockito — levanta un 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. Pruebas E2E: ciclo completo de la aplicación
Las pruebas end-to-end verifican el funcionamiento del sistema en su totalidad: desde el in