Rust fue diseñado desde el principio con un enfoque en la confiabilidad y la seguridad, por lo que las pruebas modulares se han convertido en una parte integral del ecosistema del lenguaje. Las pruebas están integradas a nivel de lenguaje y de herramientas (cargo test), lo que las convierte en una parte orgánica del proceso de desarrollo.
Historia de la cuestión
En ecosistemas tradicionales (como C/C++, Python, Java), la prueba existía como algo externo al programa mismo. En Rust, las pruebas son parte del código, se compilan y se verifican como un módulo completo. Esta sinergia se logra gracias a las construcciones del lenguaje y a las características del compilador.
Problema
Sin pruebas adecuadas, no se puede garantizar la confiabilidad de las funciones clave. Para proyectos complejos y multi-módulo, a menudo surge la pregunta: ¿cómo organizar las pruebas de manera conveniente, sin que dependan del estado de otros módulos y sin complicar la estructura del proyecto?
Solución
En Rust, las pruebas se colocan dentro del mismo archivo fuente (mediante #[cfg(test)]), o en una carpeta separada tests para pruebas de integración. Se pueden agregar pruebas privadas a cada módulo, que tienen acceso a la API privada.
Ejemplo de código:
pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, -3), -4); } }
Características clave:
¿Para qué sirve use super::*; en el módulo de prueba?
Para que las pruebas puedan acceder a las funciones y estructuras del módulo actual (incluidas las privadas), en la prueba generalmente se usa use super::*;.
¿Puede #[test] ser asíncrono?
Por defecto, #[test] no soporta async en el lenguaje, pero mediante crates externos (como tokio o async-std) se pueden hacer pruebas async #[test].
Ejemplo de código:
#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }
¿Pueden las pruebas modificar el estado global y cómo evitarlo?
Las pruebas en Rust se ejecutan de forma paralela por defecto, por lo que no se puede utilizar un estado global compartido sin sincronización (static mut), de lo contrario, surgirán condiciones de carrera.
Todas las pruebas utilizan una variable global:
static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }
Ventajas:
Desventajas:
Cada prueba está aislada, utiliza variables locales y objetos mock.
Ventajas:
Desventajas: