Introducción: Por qué las pruebas en Rust no son aburridas
Si alguna vez has escrito pruebas para código en Python o JavaScript, seguramente te has encontrado con esta situación: tu método llama a una API externa, lee un archivo del disco o accede a una base de datos. Ejecutar esas pruebas en un entorno real es un dolor. Son lentas, poco fiables (¿y si la API falla?) y requieren configuración del entorno.
En Rust la situación no es diferente. Solo que Rust es un lenguaje de sistemas, y aquí las pruebas deben ser rápidas, fiables y deterministas. Para eso precisamente se necesitan los objetos mock y los fixtures.
En este artículo veremos cómo escribir pruebas en Rust que no dependan del mundo exterior. Aprenderás a sustituir dependencias, preparar datos para las pruebas y hacerlo de forma elegante. ¡Allá vamos!
¿Qué son los objetos mock y los fixtures?
Objetos Mock (mocks)
Un objeto mock es una imitación de un componente real del sistema. Creas un objeto que se comporta como necesitas para la prueba: devuelve valores específicos, lanza errores o verifica que sus métodos fueron llamados con los argumentos correctos.
Imagina que tienes un servicio de envío de correos electrónicos. En la prueba no quieres enviar correos reales; usas un mock que simplemente verifica que el servicio fue llamado con la dirección y el texto correctos.
Fixtures
Los fixtures son datos o estados preparados que se necesitan para ejecutar una prueba. Por ejemplo, una base de datos de prueba, un archivo de configuración, estructuras predefinidas. En Rust, los fixtures a menudo se implementan mediante funciones normales que devuelven objetos listos.
Juntos, los mocks y los fixtures te dan control total sobre el entorno de la prueba. Pruebas solo tu lógica, no el funcionamiento de servicios externos.
Principios básicos de las pruebas en Rust
Antes de sumergirnos en los mocks, recordemos cómo funcionan las pruebas en Rust. Una prueba unitaria típica se ve así:
#[cfg(test)]mod tests { use super::*;
#[test] fn test_addition() { assert_eq!(2 + 2, 4); }}Muy simple. Pero cuando tu código comienza a depender de recursos externos (archivos, solicitudes HTTP, bases de datos), aparecen los problemas. La solución es invertir las dependencias y usar el patrón Dependency Injection (inyección de dependencias).
Ejemplo práctico: servicio de carga de datos
Supongamos que tenemos una estructura DataLoader que carga datos desde una API externa y los procesa. Queremos probar el procesamiento sin llamar a la API real.
Primero definimos un trait (interfaz) para la fuente de datos:
pub trait DataSource { fn fetch_data(&self) -> Result<String, String>;}Ahora implementamos la fuente real, que hace una solicitud HTTP (simplificada):
pub struct RealDataSource { pub url: String,}
impl DataSource for RealDataSource { fn fetch_data(&self) -> Result<String, String> { // Aquí iría una solicitud HTTP real Ok(format!("Datos desde {}", self.url)) }}Y el servicio que usa esta fuente:
pub struct DataLoader<T: DataSource> { source: T,}
impl<T: DataSource> DataLoader<T> { pub fn new(source: T) -> Self { DataLoader { source } }
pub fn load_and_process(&self) -> Result<String, String> { let raw_data = self.source.fetch_data()?; // Algún procesamiento Ok(format!("Procesado: {}", raw_data)) }}Observa que DataLoader no depende de una implementación concreta de DataSource. Funciona con cualquier tipo que implemente este trait. Esta es la clave para la capacidad de prueba.
Creando un objeto mock manualmente
La forma más sencilla es crear una estructura de prueb