El testing en Rust reúne herramientas para comprobar automáticamente el código.
En otros lenguajes, para hacer testing tienes que elegir un framework (JUnit, PyTest, Mocha…), instalarlo, configurarlo y aprender su sintaxis específica.
En Rust, el testing viene “de serie”. La filosofía es simple: Si escribes una función, deberías poder probarla inmediatamente en el mismo archivo.
La anatomía de un test
Un test en Rust es simplemente una función anotada con el atributo #[test].
Cuando ejecutas cargo test, Rust busca todas las funciones con esta etiqueta, las compila en un binario de prueba separado y las ejecuta.
// src/lib.rs
fn sumar(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*; // Importamos todo del módulo padre
#[test]
fn prueba_suma_simple() {
let resultado = sumar(2, 2);
assert_eq!(resultado, 4);
}
}Ejecutar los tests
Abre tu terminal y escribe:
$ cargo testVerás una salida detallada indicando qué tests pasaron (ok) y cuáles fallaron (FAILED).
Macros de aserción
Dentro de un test, comprobamos que el resultado es el esperado usando macros. Si la condición no se cumple, la macro provoca un panic!, lo que Rust interpreta como “test fallido”.
assert!(condición)
Verifica que un valor booleano sea true.
#[test]
fn prueba_booleana() {
let es_mayor = 10 > 5;
assert!(es_mayor); // Pasa si es true, falla si es false
}assert_eq!(a, b) y assert_ne!(a, b)
Verifican igualdad (equal) o desigualdad (not equal).
Son mejores que assert!(a == b) porque si fallan, te muestran ambos valores (el esperado y el obtenido), lo que facilita mucho la depuración.
#[test]
fn prueba_igualdad() {
let resultado = 2 + 2;
assert_eq!(resultado, 4); // Compara ambos lados y el test pasa
}Mensajes personalizados
Todas estas macros aceptan argumentos extra para imprimir un mensaje personalizado si fallan.
let resultado = sumar(2, 3);
assert_eq!(resultado, 5, "La función sumar está rota con inputs 2 y 3");Tests que deben fallar: #[should_panic]
A veces quieres asegurarte de que tu código reacciona correctamente ante errores, es decir, que entra en pánico cuando debe (por ejemplo, al validar argumentos inválidos).
Para eso usamos el atributo #[should_panic].
pub fn dividir(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("No dividirás por cero");
}
a / b
}
#[test]
#[should_panic(expected = "No dividirás por cero")] // Opcional: verificar mensaje
fn prueba_division_cero() {
dividir(10, 0); // Esto debe explotar para que el test pase
}Si la función dividir NO entra en pánico, entonces el test fallará (porque esperábamos un pánico y no ocurrió).
Organización de tests unitarios
La convención en Rust es escribir los tests unitarios en el mismo archivo que el código que prueban.
Para no ensuciar el código fuente compilado, se suelen meter dentro de un submódulo llamado tests anotado con #[cfg(test)].
// src/main.rs o src/lib.rs
// --- CÓDIGO REAL ---
fn funcion_privada() -> i32 { 5 }
// --- TESTS ---
#[cfg(test)] // Solo compilar este módulo al hacer 'cargo test'
mod tests {
use super::*; // Trae las funciones del padre al ámbito
#[test]
fn test_internas() {
// Al estar en el mismo archivo, ¡podemos testear funciones privadas!
assert_eq!(funcion_privada(), 5);
}
}El atributo #[cfg(test)] le dice al compilador: “Si vas a compilar para producción (cargo build), ignora este módulo por completo. Solo inclúyelo si estoy ejecutando tests”. Esto ahorra espacio y tiempo de compilación en el binario final.
Controlando la ejecución
cargo test compila y ejecuta todo en paralelo. Pero a veces quieres más control:
- Mostrar salida de
println!: Por defecto, Rust oculta lo que tus funciones imprimen si el test pasa. Para verlo siempre:cargo test -- --show-output - Ejecutar un solo test:
cargo test nombre_del_test - Ejecutar secuencialmente (si tus tests tocan archivos o bases de datos y se pisan entre sí):
cargo test -- --test-threads=1