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.
Escribir una prueba básica
Sección titulada «Escribir una prueba básica»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ónadd), para que la prueba pueda llamarla.#[test]marca una función concreta como una prueba quecargo testdebe ejecutar.
Ejecutar las pruebas
Sección titulada «Ejecutar las pruebas»Desde la carpeta de tu proyecto:
cargo testVerás algo parecido a esto:
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredCargo 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.
assert! y sus compañeras
Sección titulada «assert! y sus compañeras»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ó).
Una prueba que falla, y cómo se ve
Sección titulada «Una prueba que falla, y cómo se ve»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: 5Este 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.
Comprobar que una función hace panic
Sección titulada «Comprobar que una función hace panic»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); }}Mejores mensajes cuando algo falla
Sección titulada «Mejores mensajes cuando algo falla»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 = 57 / 2 = 3La 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 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outLas 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: 4Y 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"¿Vale la pena escribir pruebas?
Sección titulada «¿Vale la pena escribir pruebas?»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.
Práctica
Sección titulada «Práctica»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).
1. Prueba una función de calificaciones
Sección titulada «1. Prueba una función de calificaciones»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 -> A85 -> B72 -> C40 -> Fcargo 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)NoneCompá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.
Resumen
Sección titulada «Resumen»- Marca una función con
#[test]para convertirla en una prueba, y ejecuta todas las pruebas concargo test. assert!,assert_eq!yassert_ne!cubren casi todas las comprobaciones del día a día. Prefiereassert_eq!/assert_ne!, porque dan mejores mensajes cuando fallan.#[should_panic]comprueba que una función hace panic cuando debe hacerlo. Añadeexpected = "..."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 que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Cómo escribir pruebas — El libro de Rust (en inglés):
#[test], las macros assert, los mensajes propios,should_panicy las pruebas que devuelvenResult. - Controlar cómo se ejecutan las pruebas — El libro de Rust (en inglés): ejecutar una sola prueba por su nombre, ver la salida de
println!y#[ignore]. - Organización de las pruebas — El libro de Rust (en inglés): la diferencia entre las pruebas unitarias y las pruebas de integración en una carpeta
tests/. - Unit testing — Rust by Example (en inglés): ejemplos cortos de pruebas que pasan, que fallan y que hacen panic.
cargo test— The Cargo Book (en inglés): todas las opciones del comandocargo test.