Aprende Rust

Lección 25 de 26 Concurrencia y ecosistema

Tests en Rust

  • 4 min de lectura
  • 3 ejercicios
  1. Un test es una función con #[test]. assert_eq! comprueba que sumar(2, 3) devuelve lo esperado: 5.
  2. cargo test compila y ejecuta todos los tests. Si ninguna aserción falla, el test pasa: ok.
  3. Si rompes sumar (a - b en vez de a + b), el test falla y te enseña lo obtenido y lo esperado.
  4. Arreglas el código, vuelves a pasar los tests y todo en verde. Son tu red de seguridad al cambiar código.
1/4

El compilador de Rust detecta muchísimos errores, pero no puede saber si tu código hace lo que debe: si sumar(2, 3) devuelve 5 o 6 es cosa tuya. Para eso están los tests: funciones que ejecutan tu código y comprueban que el resultado es el esperado.

En Rust no necesitas instalar ninguna librería: los tests forman parte del lenguaje y Cargo sabe ejecutarlos con un solo comando.

Tu primer test

Un test es una función marcada con el atributo #[test]. Lo habitual es escribirlos en el mismo fichero que el código, dentro de un módulo tests:

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

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

    #[test]
    fn suma_dos_numeros() {
        assert_eq!(sumar(2, 3), 5);
    }

    #[test]
    fn suma_negativos() {
        assert_eq!(sumar(-2, -3), -5);
    }
}
  • #[cfg(test)] hace que el módulo solo se compile al ejecutar los tests. No ocupa nada en tu programa final.
  • use super::*; trae al módulo tests todo lo del módulo padre, incluidas las funciones privadas. Sí: en Rust puedes testear funciones privadas.
  • Un test pasa si termina sin hacer panic y falla si hace panic. Las macros assert hacen panic cuando la comprobación no se cumple.

Ejecutar los tests con cargo test

cargo test

Cargo compila el proyecto en modo test, ejecuta cada función #[test] y te enseña un resumen:

   Compiling calculadora v0.1.0 (/home/ana/calculadora)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running unittests src/lib.rs (target/debug/deps/calculadora-6112fc341e929dc9)

running 2 tests
test tests::suma_dos_numeros ... ok
test tests::suma_negativos ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Si rompes la función, por ejemplo cambiando a + b por a - b, los tests lo detectan y te dicen qué valor se obtuvo (left) y cuál se esperaba (right):

running 2 tests
test tests::suma_negativos ... FAILED
test tests::suma_dos_numeros ... FAILED

failures:

---- tests::suma_negativos stdout ----

thread 'tests::suma_negativos' panicked at src/lib.rs:16:9:
assertion `left == right` failed
  left: 1
 right: -5

---- tests::suma_dos_numeros stdout ----

thread 'tests::suma_dos_numeros' panicked at src/lib.rs:11:9:
assertion `left == right` failed
  left: -1
 right: 5

failures:
    tests::suma_dos_numeros
    tests::suma_negativos

test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Los tests se ejecutan en paralelo, en varios hilos, así que no deben depender unos de otros. Algunas variantes útiles del comando:

ComandoQué hace
cargo test sumaEjecuta solo los tests cuyo nombre contiene suma
cargo test -- --nocaptureMuestra los println! de los tests que pasan
cargo test -- --ignoredEjecuta solo los tests marcados con #[ignore]
cargo test --test integracionEjecuta un fichero concreto de tests/

Las macros de aserción

Tienes tres macros, y las tres admiten un mensaje opcional con la misma sintaxis que println!:

  • assert!(condición): falla si la condición es false.
  • assert_eq!(a, b): falla si a y b son distintos.
  • assert_ne!(a, b): falla si a y b son iguales.

No son exclusivas de los tests: puedes usarlas en cualquier parte del código.

fn doble(n: i32) -> i32 {
    n * 2
}

fn main() {
    assert!(doble(2) > 0);
    assert_eq!(doble(3), 6);
    assert_ne!(doble(5), 5, "el doble de 5 no puede ser 5");
    assert_eq!(doble(-4), -8, "doble({}) ha fallado", -4);
    println!("Todas las aserciones pasan");
}

Cuando una falla, el mensaje aparece junto a los dos valores comparados:

fn main() {
    let resultado = 2 + 2;
    assert_eq!(resultado, 5, "las matemáticas han fallado");
}
thread 'main' panicked at src/main.rs:3:5:
assertion `left == right` failed: las matemáticas han fallado
  left: 4
 right: 5

Requisito: Para usar assert_eq! y assert_ne! con tus tipos, deben implementar PartialEq (para compararlos) y Debug (para mostrarlos). Normalmente basta con #[derive(PartialEq, Debug)].

Tests que esperan un panic

A veces lo correcto es que el código falle. Con #[should_panic] el test pasa solo si la función hace panic. Añade expected para comprobar además que el mensaje contiene un texto concreto:

pub struct Porcentaje(u8);

impl Porcentaje {
    pub fn new(valor: u8) -> Porcentaje {
        if valor > 100 {
            panic!("El porcentaje debe estar entre 0 y 100 y es {valor}");
        }
        Porcentaje(valor)
    }
}

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

    #[test]
    #[should_panic(expected = "entre 0 y 100")]
    fn porcentaje_mayor_de_100() {
        Porcentaje::new(150);
    }
}

Tests que devuelven Result

Un test también puede devolver Result<(), E>. Así puedes usar el operador ? dentro: si algo devuelve Err, el test falla.

#[test]
fn parsea_un_numero() -> Result<(), std::num::ParseIntError> {
    let numero: i32 = "42".parse()?;
    assert_eq!(numero, 42);
    Ok(())
}

Ignorar tests lentos

Si un test tarda mucho, márcalo con #[ignore]. cargo test lo saltará y solo se ejecutará cuando lo pidas con cargo test -- --ignored:

#[test]
#[ignore]
fn prueba_muy_lenta() {
    // ...
}

Tests de integración

Los tests de dentro de src/ son unitarios: prueban piezas pequeñas, incluso privadas. Los tests de integración prueban tu librería desde fuera, como la usaría otra persona. Van en una carpeta tests/ junto a src/:

calculadora/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    └── integracion.rs
// tests/integracion.rs
use calculadora::sumar;

#[test]
fn suma_desde_fuera() {
    assert_eq!(sumar(10, 5), 15);
}

Cada fichero de tests/ es un crate independiente: solo ve la API pública (pub) y no necesita #[cfg(test)]. Funcionan con proyectos de librería, los que tienen src/lib.rs.

Doc tests

Los ejemplos que escribes en los comentarios de documentación /// también se ejecutan como tests. Así la documentación nunca se queda desactualizada:

/// Suma dos números.
///
/// ```
/// let total = calculadora::sumar(2, 3);
/// assert_eq!(total, 5);
/// ```
pub fn sumar(a: i32, b: i32) -> i32 {
    a + b
}

cargo test ejecuta los tres tipos de tests, uno detrás de otro: unitarios, de integración y doc tests.

En este curso: El editor de la web ejecuta main, no cargo test, así que los bloques con #[cfg(test)] no se ejecutan aquí. En los ejercicios, el corrector hace de cargo test: ejecuta comprobaciones ocultas con assert_eq! sobre tus funciones.

Hora de practicar

Ejercicios

0/3 resueltos

Reto 1 de 3

La función celsius_a_fahrenheit ya funciona. Escribe en main las aserciones que la prueban: un assert_eq! que compruebe que 0 ºC son 32 ºF, otro assert_eq! con mensaje que compruebe que 100 ºC son 212 ºF, un assert_ne! que compruebe que 10 ºC no son 10 ºF y un assert! que compruebe que 37 ºC son más de 90 ºF.
Salida esperada
Todas las aserciones pasan
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos