Lección 25 de 26 Concurrencia y ecosistema
Tests en Rust
- Un test es una función con #[test]. assert_eq! comprueba que sumar(2, 3) devuelve lo esperado: 5.
- cargo test compila y ejecuta todos los tests. Si ninguna aserción falla, el test pasa: ok.
- Si rompes sumar (a - b en vez de a + b), el test falla y te enseña lo obtenido y lo esperado.
- Arreglas el código, vuelves a pasar los tests y todo en verde. Son tu red de seguridad al cambiar código.
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óduloteststodo 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
asserthacen panic cuando la comprobación no se cumple.
Ejecutar los tests con cargo test
cargo testCargo 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.00sSi 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.00sLos tests se ejecutan en paralelo, en varios hilos, así que no deben depender unos de otros. Algunas variantes útiles del comando:
| Comando | Qué hace |
|---|---|
cargo test suma | Ejecuta solo los tests cuyo nombre contiene suma |
cargo test -- --nocapture | Muestra los println! de los tests que pasan |
cargo test -- --ignored | Ejecuta solo los tests marcados con #[ignore] |
cargo test --test integracion | Ejecuta 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 esfalse.assert_eq!(a, b): falla siaybson distintos.assert_ne!(a, b): falla siaybson 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: 5Requisito: Para usar
assert_eq!yassert_ne!con tus tipos, deben implementarPartialEq(para compararlos) yDebug(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, nocargo test, así que los bloques con#[cfg(test)]no se ejecutan aquí. En los ejercicios, el corrector hace decargo test: ejecuta comprobaciones ocultas conassert_eq!sobre tus funciones.
Hora de practicar
Ejercicios
0/3 resueltos
Reto 1 de 3 Resuelto
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.Todas las aserciones pasan
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.