En Rust, hacer pruebas está integrado directamente en el lenguaje, lo que facilita verificar el comportamiento del código sin necesidad de frameworks externos. Esto, si aplicas técnicas como TDD, te permite mantener el ciclo de desarrollo ágil y enfocado en la calidad desde el principio.
Para ejecutar un test en Rust, solo necesitas marcar una función como prueba usando el atributo #[test]
. Al usar este atributo, Rust sabe que esa función debe ejecutarse cuando corres el comando cargo test
, que es la herramienta para gestionar pruebas en Rust.
#[test]
fn foo() {
// some code...
}
Lenguaje del código: Rust (rust)
Funciones de ejemplo
Para mostrar las diferentes aserciones disponibles en Rust, vamos a usar dos funciones sencillas: una que suma dos números y otra que verifica si un número es par.
fn sum(a: i32, b: i32) -> i32 {
a + b
}
fn is_even(a: i32) -> bool {
a % 2 == 0
}
Lenguaje del código: Rust (rust)
Con estas funciones, podrás ver cómo se usan los distintos macros de aserciones.
Aserciones
En Rust, existen varios macros nativos que puedes usar para hacer pruebas. A continuación, te muestro los más comunes:
assert!
El macro assert!
es el más básico y se usa para verificar que una expresión booleana sea verdadera. Si la expresión es falsa, la prueba fallará con un panic.
#[test]
fn check_if_a_number_is_even() {
assert!(is_even(6));
}
Lenguaje del código: Rust (rust)
Este macro es ideal cuando solo necesitas verificar que una condición se cumpla, sin comparar valores específicos.
assert_eq!
El macro assert_eq!
compara dos valores para verificar que sean iguales. Si no lo son, la prueba fallará mostrando ambos valores, lo que facilita depurar el problema.
#[test]
fn calculate_the_sum_of_two_numbers() {
assert_eq!(sum(3, 3), 6);
}
Lenguaje del código: Rust (rust)
Funciona para cualquier tipo que implemente el trait PartialEq
. Por ejemplo:
#[derive(Debug, PartialEq)]
pub struct Point {
pub x: i32,
pub y: i32,
}
#[test]
fn two_points_are_equal() {
let point1 = Point { x: 1, y: 2 };
let point2 = Point { x: 1, y: 2 };
assert_eq!(point1, point2);
}
Lenguaje del código: Rust (rust)
assert_ne!
Este macro es el inverso de assert_eq!
: compara dos valores y verifica que no sean iguales. Si los valores son iguales, la prueba fallará.
#[test]
fn calculate_the_sum_of_two_numbers_fail() {
assert_ne!(sum(3, 3), 5);
}
Lenguaje del código: Rust (rust)
Aserciones de depuración
Rust también tiene macros de depuración como debug_assert!
, debug_assert_eq!
y debug_assert_ne!
. Estos funcionan igual que los macros anteriores, pero solo se ejecutan en modo debug, no en compilaciones de release
. Esto puede ser útil para optimizar el rendimiento.
#[test]
fn calculate_the_sum_of_two_numbers_with_debug() {
debug_assert_eq!(sum(3, 3), 6);
}
#[test]
fn calculate_the_sum_of_two_numbers_fail_with_debug() {
debug_assert_ne!(sum(3, 3), 5);
}
#[test]
fn check_if_a_number_is_even_with_debug() {
debug_assert!(is_even(6));
}
Lenguaje del código: Rust (rust)
Otros macros
Además de los macros nativos, puedes usar librerías externas para ampliar las aserciones. Por ejemplo, la librería matches permite verificar si una expresión coincide con un patrón.
use matches::assert_matches;
#[test]
fn check_an_specific_pattern() {
let resultado: Result<i32, &str> = Ok(10);
assert_matches!(resultado, Ok(10));
}
Lenguaje del código: Rust (rust)
Organización de tests en Rust
Hay dos maneras principales de organizar tus pruebas en Rust: dentro del mismo módulo que el código productivo o en una carpeta separada llamada tests
.
Pruebas en el mismo módulo
Si colocas las pruebas en el mismo módulo, puedes probar funciones privadas y mantener las pruebas cerca del código que validan. Esto es útil en proyectos pequeños o para pruebas rápidas.
#[cfg(test)]
fn sum(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calculate_the_sum_of_two_numbers() {
assert_eq!(sum(3, 3), 6);
}
}
Lenguaje del código: Rust (rust)
Pruebas en archivos separados
Para proyectos más grandes, es mejor organizar las pruebas en una carpeta tests
. Esto te permite mantener el código de producción separado del código de prueba.
use testing_rust::testing_rust::sum;
#[test]
fn calculate_the_sum_of_two_numbers() {
assert_eq!(sum(3, 3), 6);
}
Lenguaje del código: Rust (rust)
Es una buena práctica nombrar estos archivos con el sufijo _test.rs
, por ejemplo: foo_test.rs
.
Mocks en Rust
Aunque Rust no tiene un sistema de mocks integrado, librerías como mockall permiten crear mocks fácilmente. Los mocks son útiles para simular dependencias externas, como bases de datos o servicios remotos.
Definición de la interfaz
#[derive(Debug, PartialEq)]
pub struct User {
pub user_id: i32,
pub name: String,
pub age: i32,
}
pub trait DatabaseRepository {
fn find_by_id(&self, user_id: &i32) -> bool;
}
Lenguaje del código: Rust (rust)
Uso de mockall para crear un mock
use mockall::mock;
use testing_rust::database::{DatabaseRepository, User};
mock! {
pub Database {}
impl DatabaseRepository for Database {
fn find_by_id(&self, user: &i32) -> bool;
}
}
#[test]
fn user_exist_in_database() {
let mut mock_db = MockDatabase::new();
let user = User { user_id: 1, name: "test data".to_string(), age: 20 };
mock_db.expect_find_by_id()
.with(mockall::predicate::eq(user.user_id.clone()))
.times(1)
.returning(|_| true);
let result = is_user_in_database(&mock_db, &user);
assert!(result);
}
fn is_user_in_database(db: &dyn DatabaseRepository, user: &User) -> bool {
db.find_by_id(&user.user_id)
}
Lenguaje del código: Rust (rust)
En este caso, hemos utilizado el macro mock!
para generar un mock de nuestra interfaz DatabaseRepository
, y luego lo hemos utilizado para simular el comportamiento de nuestra base de datos.
Si prefieres mantener los mocks dentro del mismo módulo que tu código, puedes usar el atributo #[automock]
para generar los mocks directamente a partir de la interfaz:
use mockall::{automock, mock};
use testing_rust::database::User;
#[automock]
trait DatabaseRepository {
fn find_by_id(&self, user_id: &i32) -> bool;
}
Lenguaje del código: Rust (rust)
De esta forma, Rust te proporciona un mock automático sin tener que crear uno manualmente.
Conclusión
Rust proporciona una infraestructura robusta y flexible para pruebas, integrada directamente en el lenguaje. Ya sea que estés escribiendo pruebas unitarias sencillas con assert!
o pruebas más complejas con mocks usando mockall
, el ecosistema de pruebas en Rust te permite asegurarte de que tu código es confiable y robusto.
Existen muchas más herramientas externas para realizar otros tipos de tests, como proptest para property based testing, o httpmock para hacer tests de integración con apis http. ¡Estas y otras herramientas pueden ser exploradas en un post futuro!
Si queréis revisar más cosillas sobre Rust, podéis hecharle un ojo a estos artículos: