Testing avanzado en Rust: tests de integración, mockall para mocks y proptest para property testing

Rust facilita el testing desde el primer momento con soporte integrado en cargo test. Más allá de los unit tests básicos, el ecosistema ofrece herramientas para tests de integración, mocks automáticos, property-based testing y benchmarks estadísticos, todo ello sin salir del flujo habitual de trabajo con Cargo.

Tests de integración en el directorio tests/

Los tests unitarios viven en el mismo archivo que el código, dentro de #[cfg(test)]. Los tests de integración van en el directorio tests/ en la raíz del crate: cada archivo es un binario independiente que importa la biblioteca como si fuera un usuario externo. Solo tienen acceso a la API pública.

// src/lib.rs
pub fn dividir(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        return Err("División por cero".into());
    }
    Ok(a / b)
}

pub fn raiz_cuadrada(x: f64) -> Result<f64, String> {
    if x < 0.0 {
        return Err(format!("{x} es negativo; raíz no definida en reales"))
    }
    Ok(x.sqrt())
}
// tests/matematicas.rs
use mi_crate::{dividir, raiz_cuadrada};

#[test]
fn test_division_ok() {
    assert!((dividir(10.0, 4.0).unwrap() - 2.5).abs() < f64::EPSILON);
}

#[test]
fn test_division_por_cero() {
    assert!(dividir(5.0, 0.0).is_err());
}

#[test]
fn test_raiz_negativa() {
    let err = raiz_cuadrada(-4.0).unwrap_err();
    assert!(err.contains("negativo"));
}

#[test]
#[should_panic(expected = "índice fuera")]
fn test_panic_esperado() {
    let v = vec![1, 2, 3];
    let _ = v[10]; // debe hacer panic
}

Mocks automáticos con mockall

mockall genera implementaciones mock de traits con el atributo #[automock]. Los mocks permiten aislar la unidad bajo prueba de sus dependencias externas (base de datos, red, sistema de archivos) y verificar que se llaman los métodos correctos con los argumentos correctos.

// [dev-dependencies]
// mockall = "0.13"

use mockall::{automock, predicate::*};

#[automock]
trait RepositorioUsuarios {
    fn buscar(&self, id: u32) -> Option<String>;
    fn guardar(&mut self, id: u32, nombre: &str) -> bool;
}

fn saludar_usuario(repo: &dyn RepositorioUsuarios, id: u32) -> String {
    match repo.buscar(id) {
        Some(nombre) => format!("Hola, {nombre}!"),
        None => "Usuario no encontrado".into(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_usuario_encontrado() {
        let mut mock = MockRepositorioUsuarios::new();
        mock.expect_buscar()
            .with(eq(42u32))
            .times(1)
            .returning(|_| Some("Alice".into()));

        let resultado = saludar_usuario(&mock, 42);
        assert_eq!(resultado, "Hola, Alice!");
    }

    #[test]
    fn test_usuario_no_encontrado() {
        let mut mock = MockRepositorioUsuarios::new();
        mock.expect_buscar()
            .returning(|_| None);

        let resultado = saludar_usuario(&mock, 99);
        assert_eq!(resultado, "Usuario no encontrado");
    }
}

Property-based testing con proptest

proptest genera automáticamente cientos de casos de prueba a partir de estrategias de generación de datos, buscando valores que falsifiquen las propiedades definidas. Cuando encuentra un fallo, reduce (shrinks) el caso hasta el mínimo que lo reproduce.

// [dev-dependencies]
// proptest = "1"

#[cfg(test)]
mod prop_tests {
    use proptest::prelude::*;

    fn invertir(v: &[i32]) -> Vec<i32> {
        v.iter().rev().cloned().collect()
    }

    proptest! {
        // Propiedad: invertir dos veces devuelve el original
        #[test]
        fn prop_doble_inversion(v in prop::collection::vec(any::<i32>(), 0..100)) {
            let resultado = invertir(&invertir(&v));
            prop_assert_eq!(resultado, v);
        }

        // Propiedad: la suma no cambia al invertir
        #[test]
        fn prop_suma_invariante(v in prop::collection::vec(-1000i32..=1000, 1..50)) {
            let suma_orig: i32 = v.iter().sum();
            let suma_inv: i32 = invertir(&v).iter().sum();
            prop_assert_eq!(suma_orig, suma_inv);
        }

        // Propiedad: ordenar es idempotente
        #[test]
        fn prop_sort_idempotente(mut v in prop::collection::vec(any::<i32>(), 0..200)) {
            v.sort();
            let v2 = v.clone();
            v.sort();
            prop_assert_eq!(v, v2);
        }
    }
}

Benchmarks con criterion

criterion es el estándar de facto para benchmarks estadísticos en Rust. A diferencia del inestable #[bench] de la librería estándar, criterion funciona en stable, calienta la CPU antes de medir y realiza análisis estadístico para detectar cambios de rendimiento entre ejecuciones.

// [dev-dependencies]
// criterion = { version = "0.5", features = ["html_reports"] }

// [[bench]]
// name = "mi_benchmark"
// harness = false

// benches/mi_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn benchmark_fibonacci(c: &mut Criterion) {
    c.bench_function("fibonacci 20", |b| b.iter(|| fibonacci(black_box(20))));
    c.bench_function("fibonacci 25", |b| b.iter(|| fibonacci(black_box(25))));
}

criterion_group!(benches, benchmark_fibonacci);
criterion_main!(benches);

Cobertura con cargo-tarpaulin

cargo-tarpaulin instrumenta el binario de tests y genera informes de cobertura de línea. Es la opción más sencilla para Linux sin necesitar nightly.

// Instalación y uso:
// cargo install cargo-tarpaulin
// cargo tarpaulin --out Html --output-dir coverage/

// Antipatrones frecuentes en testing:
// 1. Tests que solo comprueban que no hace panic (sin assert!)
// 2. Tests que dependen del orden de ejecución entre sí
// 3. Tests que tocan el sistema de archivos real sin limpiar después
// 4. Mockear demasiado: si el mock es más complejo que el código real,
//    es señal de que la abstracción necesita rediseño

La estrategia más productiva combina los tres niveles: unit tests para la lógica pura, property tests para las invariantes algebraicas, y tests de integración para los flujos completos con dependencias reales (o mocks ligeros cuando estas dependencias son lentas o no deterministas).

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP