Testing en Rust: unit tests, integration tests, doc tests y benchmarks con Criterion

Rust incluye un framework de testing integrado en cargo test. No necesitas librerías externas para los casos habituales: unit tests junto al código, integration tests en un directorio separado y doc tests en los comentarios de documentación. Para benchmarks de rendimiento, Criterion es el estándar de facto.

Unit tests: junto al código, acceso a funciones privadas

// src/lib.rs

fn sumar(a: i32, b: i32) -> i32 {
    a + b
}

fn dividir(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

#[cfg(test)]
mod tests {
    use super::*; // accede a funciones privadas del módulo padre

    #[test]
    fn test_sumar() {
        assert_eq!(sumar(2, 3), 5);
        assert_eq!(sumar(-1, 1), 0);
    }

    #[test]
    fn test_dividir_normal() {
        assert_eq!(dividir(10.0, 2.0), Some(5.0));
    }

    #[test]
    fn test_dividir_por_cero() {
        assert_eq!(dividir(5.0, 0.0), None);
    }

    #[test]
    #[should_panic(expected = "división")]
    fn test_panic() {
        panic!("división por cero");
    }

    #[test]
    #[ignore = "requiere conexión a base de datos"]
    fn test_lento() {
        // cargo test -- --include-ignored para ejecutarlo
    }
}

Integration tests: en el directorio tests/

// tests/integracion.rs
// Solo puede usar la API pública de la librería

use mi_crate::{sumar, dividir};

#[test]
fn suma_positivos() {
    assert_eq!(sumar(10, 20), 30);
}

#[test]
fn dividir_resultado_correcto() {
    let resultado = dividir(9.0, 3.0).unwrap();
    assert!((resultado - 3.0).abs() < f64::EPSILON);
}

// Módulo de utilidades compartido entre tests
// tests/common/mod.rs
pub fn datos_prueba() -> Vec<i32> {
    vec![1, 2, 3, 4, 5]
}

// tests/otro_test.rs
mod common;
#[test]
fn usar_utilidades() {
    let d = common::datos_prueba();
    assert_eq!(d.len(), 5);
}

Doc tests: ejemplos ejecutables en la documentación

/// Suma dos números enteros.
///
/// # Ejemplos
///
/// ```
/// let resultado = mi_crate::sumar(2, 3);
/// assert_eq!(resultado, 5);
/// ```
///
/// ```
/// // También con negativos
/// assert_eq!(mi_crate::sumar(-1, 1), 0);
/// ```
pub fn sumar(a: i32, b: i32) -> i32 {
    a + b
}

/// Muestra código que no compila (no se ejecuta como test)
///
/// ```compile_fail
/// let x: i32 = "esto no compila";
/// ```
///
/// Código que no se ejecuta pero se muestra:
/// ```no_run
/// mi_crate::conectar_bbdd(); // requiere servidor real
/// ```
pub fn placeholder() {}

Aserciones útiles

#[cfg(test)]
mod tests {
    #[test]
    fn aserciones() {
        // assert_eq! y assert_ne!
        assert_eq!(2 + 2, 4, "la suma falló con valor {}", 2 + 2);
        assert_ne!("hola", "adios");

        // assert! para condiciones booleanas
        assert!(vec![1, 2, 3].contains(&2));

        // Comparar floats con tolerancia
        let pi = std::f64::consts::PI;
        assert!((pi - 3.14159).abs() < 0.0001);

        // Verificar Result y Option
        let resultado: Result<i32, &str> = Ok(42);
        assert!(resultado.is_ok());
        assert_eq!(resultado.unwrap(), 42);
    }

    #[test]
    fn test_retorna_result() -> Result<(), String> {
        // Los tests pueden devolver Result; Err se muestra como fallo
        "42".parse::<i32>().map_err(|e| e.to_string())?;
        Ok(())
    }
}

Benchmarks con Criterion

// Cargo.toml
// [dev-dependencies]
// criterion = { version = "0.5", features = ["html_reports"] }
//
// [[bench]]
// name = "mi_benchmark"
// harness = false

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

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

fn benchmark_fibonacci(c: &mut Criterion) {
    let mut group = c.benchmark_group("fibonacci");

    for i in [10u64, 15, 20].iter() {
        group.bench_with_input(BenchmarkId::from_parameter(i), i, |b, &n| {
            b.iter(|| fibonacci(n));
        });
    }
    group.finish();
}

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

// Ejecutar: cargo bench
// Genera informe HTML en target/criterion/

Comandos útiles de cargo test

// Ejecutar todos los tests
// cargo test

// Ejecutar solo tests que contengan "sumar" en el nombre
// cargo test sumar

// Mostrar output de println! (normalmente capturado)
// cargo test -- --nocapture

// Ejecutar tests ignorados
// cargo test -- --include-ignored

// Ejecutar un único test por nombre exacto
// cargo test tests::test_sumar -- --exact

// Tests de integración en un fichero concreto
// cargo test --test integracion

Resumen

  • Los unit tests van en el mismo fichero con #[cfg(test)]; tienen acceso a items privados.
  • Los integration tests van en tests/ y solo ven la API pública.
  • Los doc tests en comentarios /// se ejecutan con cargo test y mantienen los ejemplos sincronizados con el código.
  • #[should_panic], #[ignore] y devolver Result son las anotaciones más útiles.
  • Criterion provee benchmarks estadísticamente robustos con informes HTML.

COMPARTE ESTE ARTÍCULO

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