Testing en Rust
Rust integra un ejecutor de tests directamente en el toolchain, así que un test es solo una función con un atributo encima — sin framework externo, sin archivo de configuración.
Todo lenguaje eventualmente necesita una historia para el testing, y la de Rust es inusualmente buena: el ejecutor de tests viene con `cargo`, las macros de aserción vienen con la biblioteca estándar, y la convención de dónde viven los tests está integrada en el sistema de módulos que aprendiste en las últimas lecciones. No hay un equivalente a instalar `pytest` al que recurrir — `cargo new` ya genera un proyecto listo para contener tests, y `cargo test` ya sabe cómo encontrarlos y ejecutarlos. Eso importa más de lo que parece: cuando el testing tiene costo de configuración cero, realmente escribes tests, en lugar de posponerlos hasta que el proyecto sea 'lo bastante grande como para necesitarlos'.
Un test es una función normal marcada con `#[test]` encima. `cargo test` compila tu crate en un modo especial habilitado para tests, encuentra cada función marcada con `#[test]`, ejecuta cada una y reporta si pasó o falló — un test 'pasa' simplemente por no entrar en panic, y 'falla' en el instante en que lo hace. Los tests se ejecutan en paralelo entre hilos por defecto, lo cual es rápido pero significa que dos tests que tocan el mismo recurso global (un archivo, una variable de entorno, una base de datos compartida) pueden interferir entre sí; cuando eso ocurre, `cargo test -- --test-threads=1` los obliga a correr de uno en uno mientras rastreas el problema real.
Las macros que usarás dentro de casi todos los tests son `assert!`, `assert_eq!` y `assert_ne!`. `assert!(condición)` entra en panic con un mensaje genérico si `condición` es `false`; `assert_eq!(izquierda, derecha)` entra en panic con un mensaje que muestra ambos valores si no son iguales, lo cual es mucho más útil para depurar que un simple `assert!(izquierda == derecha)`, porque no tienes que agregar tu propio print para ver qué salió mal realmente. `assert_ne!` es la imagen espejo: entra en panic si los dos valores *son* iguales. Las tres aceptan un mensaje personalizado opcional como argumentos extra, formateado igual que `println!` formatea los suyos, para cuando la salida de fallo por defecto no da suficiente contexto.
Por convención, los tests unitarios — que verifican una función o un módulo de forma aislada, a menudo accediendo a detalles de implementación privados — viven en el mismo archivo que el código que prueban, dentro de un bloque `#[cfg(test)] mod tests`. El atributo `#[cfg(test)]` le dice al compilador que compile ese módulo solo cuando se ejecutan los tests, así que nada de tu código de test infla el binario de producción. Dentro de él, `use super::*;` trae al scope todos los ítems del módulo padre, por eso el patrón se ve casi idéntico de un archivo a otro: un módulo pequeño al final del archivo, probando las funciones definidas arriba, sin necesidad de reimportar cada una por su nombre.
Los tests unitarios pueden ver funciones privadas porque viven dentro del crate, pero no pueden decirte si la API *pública* de tu crate realmente funciona como la usaría un usuario externo — y para eso están los tests de integración. Cualquier archivo que coloques en un directorio `tests/` en la raíz de tu proyecto (hermano de `src/`) se compila como su propio crate independiente que solo puede ver lo que hayas marcado `pub`, exactamente como lo vería un dependiente real de tu biblioteca. Cada archivo en `tests/` se ejecuta de forma independiente, y juntos son lo más parecido que tiene Rust a 'este crate realmente funciona desde afuera'.
Por último, algunas funciones *deben* entrar en panic ante entradas incorrectas específicas, y necesitas probar que lo hacen. `#[should_panic]` invierte la lógica habitual de pasa/falla de un test: pasa solo si la función entra en panic, y falla si termina normalmente. Puedes acotarlo más con `#[should_panic(expected = "alguna subcadena")]`, que además verifica que el mensaje de panic contenga esa subcadena — sin eso, un test podría 'pasar' entrando en panic por una razón completamente distinta, lo cual anula el propósito de probar ese camino de fallo específico.
pub fn add_two(a: i32, b: i32) -> i32 {a + b}pub fn is_even(n: i32) -> bool {n % 2 == 0}#[cfg(test)]mod tests {use super::*;#[test]fn adds_two_positive_numbers() {assert_eq!(add_two(2, 3), 5);}#[test]fn recognizes_even_numbers() {assert!(is_even(4));assert!(!is_even(7));}}fn main() {println!("2 + 3 = {}", add_two(2, 3));}
The #[cfg(test)] mod tests block only compiles when running cargo test, and use super::* brings add_two and is_even into scope so the tests can call them directly by name.
pub struct Percentage {value: u8,}impl Percentage {pub fn new(value: u8) -> Percentage {if value > 100 {panic!("percentage cannot exceed 100, got {value}");}Percentage { value }}}#[cfg(test)]mod tests {use super::*;#[test]fn accepts_a_valid_percentage() {let p = Percentage::new(50);assert_eq!(p.value, 50);}#[test]#[should_panic(expected = "cannot exceed 100")]fn rejects_a_percentage_over_100() {Percentage::new(150);}}fn main() {let p = Percentage::new(80);println!("value: {}", p.value);}
should_panic(expected = "...") passes only if Percentage::new panics AND the panic message contains that substring, so the test would fail if the code panicked for an unrelated reason instead of the intended validation.
// Simulates the shape of a file at tests/integration_test.rs.// In a real project this file has no module wrapper around it — cargo// treats every file directly under tests/ as its own independent crate// that can only see the pub items of your library.pub fn add_two(a: i32, b: i32) -> i32 {a + b}#[test]fn public_api_adds_two_numbers() {assert_eq!(add_two(10, 15), 25);}fn main() {}
A file placed directly under tests/ needs no #[cfg(test)] wrapper at all — cargo only compiles the tests/ directory when running cargo test, and treats each file there as its own crate that can only reach the pub API.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. You write `#[test] fn parses_valid_input() { ... }` inside `src/lib.rs`, but forget to wrap it in a `#[cfg(test)] mod tests` block. What actually happens when you run `cargo build` (not `cargo test`)?