23 - Testing en Rust - garantizando la calidad del código

23 - Testing en Rust - garantizando la calidad del código

El testing es una parte fundamental del desarrollo de software, y Rust proporciona un conjunto de herramientas poderosas para escribir pruebas automatizadas. Gracias a su enfoque en la seguridad y confiabilidad, Rust permite a los desarrolladores escribir código robusto que se puede verificar con facilidad. En este artículo, exploraremos cómo realizar pruebas en Rust utilizando el framework de testing incorporado, junto con algunos conceptos avanzados.


Tipos de pruebas en Rust #

Rust soporta varios tipos de pruebas que nos ayudan a verificar el comportamiento de nuestro código:

  1. Pruebas unitarias: Validan funciones individuales.
  2. Pruebas de integración: Verifican que múltiples partes del sistema funcionan juntas.
  3. Pruebas de regresión: Aseguran que nuevas modificaciones no rompan funcionalidades existentes.
  4. Pruebas de benchmark: Evalúan el rendimiento de partes críticas del código.

Pruebas unitarias #

Las pruebas unitarias se escriben dentro del mismo archivo donde está el código fuente y suelen ubicarse en un módulo especial #[cfg(test)].

Ejemplo: calculando el daño de un ataque pokémon #

pub fn calcular_dano(ataque: u32, defensa: u32, poder: u32) -> u32 {
    let base = (ataque as f64 / defensa as f64) * poder as f64 / 50.0 + 2.0;
    base as u32
}

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

    #[test]
    fn test_calculo_dano() {
        let resultado = calcular_dano(55, 40, 60);
        assert_eq!(resultado, 21);
    }
}

📌 Puntos clave:

  • Las pruebas se colocan dentro de un módulo con #[cfg(test)].
  • #[test] marca la función como una prueba.
  • assert_eq! verifica que el resultado sea el esperado.

Para ejecutar las pruebas, usamos:

$ cargo test

Pruebas de integración #

Las pruebas de integración verifican cómo interactúan varias partes de un programa. Se colocan en la carpeta tests/ dentro del proyecto.

Ejemplo: verificando una pokédex #

// src/lib.rs
pub fn buscar_elemento(pokemon: &str) -> Option<&'static str> {
    let pokedex = vec!["Item1", "Item2", "Item4"];
    if pokedex.contains(&pokemon) {
        Some(pokemon)
    } else {
        None
    }
}

Ahora creamos una prueba de integración en tests/pokedex.rs:

// tests/pokedex.rs
use rust_project::buscar_elemento;

#[Test]
fn test_busqueda_elemento() {
    assert_eq!(buscar_elemento("Item1"), Some("Item1"));
    assert_eq!(buscar_elemento("Mewtwo"), None);
}

Para ejecutar las pruebas de integración:

$ cargo test

📌 Puntos clave:

  • Se ubican en tests/ y tienen acceso a la biblioteca del proyecto.
  • Nos permiten verificar la funcionalidad de módulos completos.

Pruebas con should_panic #

Podemos probar que una función genera un panic! esperado.

Ejemplo: capturar un pokémon sin poké balls #

pub fn lanzar_pokeball(pokeballs: u32) {
    if pokeballs == 0 {
        panic!("No tienes Poké Balls");
    }
}

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

    #[test]
    #[should_panic(expected = "No tienes Poké Balls")]
    fn test_lanzar_pokeball() {
        lanzar_pokeball(0);
    }
}

📌 Puntos clave:

  • #[should_panic] espera que la función falle.
  • expected = "mensaje" verifica que el pánico contenga el mensaje específico.

Uso de result<t, e> en pruebas #

Podemos escribir pruebas que retornen Result<T, E> en lugar de usar panic!.

#[Test]
fn test_result() -> Result<(), String> {
    if 2 + 2 == 4 {
        Ok(())
    } else {
        Err("La suma falló".into())
    }
}

📌 Beneficio: Nos permite usar ? en las pruebas para manejar errores sin hacer panic!.


Pruebas de benchmark con criterion #

Para medir el rendimiento, usamos la crate criterion.

Ejemplo: medir el rendimiento de calcular_dano #

  1. Agrega criterion a Cargo.toml:
[dev-dependencies]
criterion = "0.3"
  1. Crea benches/benchmark.rs:
use criterion::{black_box, Criterion, criterion_group, criterion_main};
use rust_project::calcular_dano;

fn benchmark(c: &mut Criterion) {
    c.bench_function("calcular_dano", |b| b.iter(|| calcular_dano(black_box(55), black_box(40), black_box(60))));
}

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

Para ejecutar benchmarks:

$ cargo bench

📌 Beneficio: Permite optimizar el código basado en métricas reales.


Conclusión #

Rust nos proporciona un sólido framework de pruebas para garantizar la calidad del código:

  • Pruebas unitarias para validar funciones individuales.
  • Pruebas de integración para verificar la interacción entre módulos.
  • Pruebas de pánico para validar errores esperados.
  • Benchmarks para evaluar rendimiento.

Al incorporar pruebas en el desarrollo, aseguramos un código más seguro, robusto y mantenible.