Ir al contenido

Pruebas en Rust

Rust trae las pruebas (tests) integradas en el propio lenguaje y en cargo, así que no necesitas instalar ninguna biblioteca aparte para empezar. En esta página verás lo justo para escribir y ejecutar tus primeras pruebas.

Una prueba es una función normal marcada con #[test]:

fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}

Vale la pena fijarse en algunas cosas:

  • #[cfg(test)] le dice a Rust que este módulo solo se compila cuando ejecutas las pruebas. No forma parte de tu programa normal.
  • use super::*; trae todo lo que hay en el módulo padre (aquí, la función add), para que la prueba pueda llamarla.
  • #[test] marca una función concreta como una prueba que cargo test debe ejecutar.

Desde la carpeta de tu proyecto:

Terminal window
cargo test

Verás algo parecido a esto:

running 1 test
test tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored

Cargo escribe este resumen en inglés: dice que ejecutó 1 prueba, que it_adds_two_numbers salió bien (ok) y que hubo 1 prueba superada, 0 fallidas y 0 ignoradas.

Con tres macros cubres casi todo lo que vas a necesitar:

#[test]
fn checks() {
assert!(2 + 2 == 4); // la prueba falla si la condición es falsa
assert_eq!(2 + 2, 4); // falla si los dos valores no son iguales
assert_ne!(2 + 2, 5); // falla si los dos valores SÍ son iguales
}

Normalmente se prefieren assert_eq! y assert_ne! en lugar de un simple assert!(a == b). Cuando fallan, muestran los dos valores, así que ves al instante qué no coincidió, en vez de un simple “assertion failed” (la aserción falló).

fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_will_fail() {
assert_eq!(add(2, 2), 5);
}
}
test tests::this_will_fail ... FAILED
assertion `left == right` failed
left: 4
right: 5

Este es justo el tipo de mensaje que quieres cuando una prueba falla. left (izquierda) es el valor que produjo tu código y right (derecha) es el que esperaba la prueba. Así ves de un vistazo qué salió y qué debería haber salido.

A veces el comportamiento correcto es un panic (cuando el programa se detiene por un error). Por ejemplo, una función que debe rechazar datos no válidos. Para probarlo existe #[should_panic]:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("no se puede dividir entre cero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

Cuando una prueba falla, quieres saber por qué lo antes posible. Dos pequeños añadidos ayudan mucho.

Un mensaje propio. assert!, assert_eq! y assert_ne! aceptan un mensaje extra después de los valores, escrito igual que en un println!. Solo se imprime si la comprobación falla.

expected en should_panic. Un #[should_panic] a secas pasa con cualquier panic, incluso con uno causado por otro error distinto. Si añades expected = "...", la prueba solo pasa si el mensaje del panic contiene ese texto.

Aquí tienes otra vez la función divide, ahora con un main para que puedas ejecutarla, y dos pruebas que usan ambas ideas:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("no se puede dividir entre cero");
}
a / b
}
fn main() {
println!("10 / 2 = {}", divide(10, 2));
println!("7 / 2 = {}", divide(7, 2));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounds_down() {
let result = divide(7, 2);
assert_eq!(result, 3, "7 / 2 debería redondear hacia abajo, pero dio {result}");
}
#[test]
#[should_panic(expected = "dividir entre cero")]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

Salida:

10 / 2 = 5
7 / 2 = 3
▶ Pruébalo tú mismo

La salida de arriba es lo que imprime cargo run, porque main no ejecuta las pruebas. Para ejecutarlas, usa cargo test. En el Rust Playground, abre el menú junto a Run y elige Test. Verás esto:

running 2 tests
test tests::rounds_down ... ok
test tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Las pruebas se ejecutan en paralelo, así que las dos líneas test ... pueden aparecer en cualquier orden. Ahora imagina que la prueba esperara 4 en lugar de 3. El fallo incluiría tu mensaje, justo al lado de los dos valores:

assertion `left == right` failed: 7 / 2 debería redondear hacia abajo, pero dio 3
left: 3
right: 4

Y si divide hiciera panic con un mensaje distinto, la comprobación de expected lo detecta. Rust te avisa (en inglés) de que el mensaje del panic no contenía el texto esperado, y te muestra los dos:

note: panic did not contain expected string
panic message: "no se puede dividir entre cero"
expected substring: "negativo"

Siendo honestos: para un script corto, seguramente no lo harás. Las pruebas empiezan a compensar cuando el código se va a reutilizar o a cambiar más adelante. Te permiten reorganizar el código (refactorizar) con confianza, porque cargo test te avisa al momento si rompiste algo, en lugar de que te enteres por un usuario.

Las pruebas también pueden vivir en tu documentación: los ejemplos de código dentro de los comentarios de documentación /// también se ejecutan con cargo test, así que tus ejemplos nunca se quedan desactualizados. La página de Documentación te enseña cómo.

En los dos ejercicios, la solución tiene un main que puedes ejecutar y pruebas que ejecutas con cargo test (o con Test en el Playground).

Escribe fn grade(score: u32) -> char que devuelva 'A' para 90 o más, 'B' de 80 a 89, 'C' de 70 a 79 y 'F' para cualquier nota menor. Después escribe pruebas que comprueben los límites, como 90 y 89. Pista: los errores se esconden en los límites. Una prueba con exactamente 90 atrapa el clásico error de escribir > en lugar de >=.

Ver solución
fn grade(score: u32) -> char {
if score >= 90 {
'A'
} else if score >= 80 {
'B'
} else if score >= 70 {
'C'
} else {
'F'
}
}
fn main() {
for score in [95, 85, 72, 40] {
println!("{score} -> {}", grade(score));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edges_of_a() {
assert_eq!(grade(90), 'A');
assert_eq!(grade(89), 'B');
}
#[test]
fn edges_of_c() {
assert_eq!(grade(70), 'C');
assert_eq!(grade(69), 'F');
}
#[test]
fn zero_is_f() {
assert_eq!(grade(0), 'F', "una nota de 0 debería suspender");
}
}

Salida:

95 -> A
85 -> B
72 -> C
40 -> F
▶ Pruébalo tú mismo

cargo test debería informar 3 passed (3 superadas). Prueba a cambiar score >= 90 por score > 90 y vuelve a ejecutar las pruebas: edges_of_a falla y te muestra left: 'B' y right: 'A'.

2. Prueba una función que devuelve un Option

Sección titulada «2. Prueba una función que devuelve un Option»

Escribe fn safe_divide(a: i32, b: i32) -> Option<i32> que devuelva None cuando b sea 0, en lugar de hacer panic. Escribe una prueba para una división normal y otra para la división entre cero. Pista: assert_eq! también funciona con Option, así que puedes escribir assert_eq!(safe_divide(1, 0), None).

Ver solución
fn safe_divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn main() {
println!("{:?}", safe_divide(10, 2));
println!("{:?}", safe_divide(1, 0));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn divides_normally() {
assert_eq!(safe_divide(10, 2), Some(5));
}
#[test]
fn zero_gives_none() {
assert_eq!(safe_divide(1, 0), None);
}
}

Salida:

Some(5)
None
▶ Pruébalo tú mismo

Compárala con la función divide de arriba. Allí, dividir entre cero es un panic, así que la prueba necesita #[should_panic]. Aquí es solo un valor de retorno normal, así que basta con un simple assert_eq!. Las funciones que devuelven Option o Result suelen ser más fáciles de probar.

  • Marca una función con #[test] para convertirla en una prueba, y ejecuta todas las pruebas con cargo test.
  • assert!, assert_eq! y assert_ne! cubren casi todas las comprobaciones del día a día. Prefiere assert_eq!/assert_ne!, porque dan mejores mensajes cuando fallan.
  • #[should_panic] comprueba que una función hace panic cuando debe hacerlo. Añade expected = "..." para comprobar que hizo panic por el motivo correcto.
  • Añade un mensaje propio a cualquier macro assert para explicar qué salió mal cuando falla.
  • Las pruebas valen más en el código que piensas cambiar o reutilizar más adelante.

Comprueba lo aprendido

1. ¿Qué atributo marca una función como una prueba que cargo test debe ejecutar?

2. ¿Por qué normalmente se prefieren assert_eq! y assert_ne! en lugar de un simple assert!(a == b)?

3. ¿Qué hace #[should_panic]?

Puntuación: 0 / 3