Controlando como los tests son ejecutados
Al igual que cargo run
compila tu código y luego ejecuta el binario
resultante, cargo test
compila tu código en modo de test y ejecuta el binario
resultante. El comportamiento por defecto del binario producido por cargo test
es ejecutar todos los tests en paralelo y capturar la salida generada durante la
ejecución de los tests, previniendo que la salida sea mostrada y haciendo más
fácil leer la salida relacionada con los resultados de los tests. Sin embargo,
puedes especificar opciones de línea de comandos para cambiar este
comportamiento por defecto.
Algunas opciones de línea de comandos van a cargo test
, y otras van al binario
de test resultante. Para separar estos dos tipos de argumentos, debes listar los
argumentos que van a cargo test
seguidos del separador --
y luego los que
van al binario de test. Ejecutar cargo test --help
muestra las opciones que
puedes usar con cargo test
, y ejecutar cargo test -- --help
muestra las
opciones que puedes usar después del separador. Esas opciones también están
documentadas en la sección "Tests" del libro de rustc.
Ejecutando tests en paralelo o consecutivamente
Cuando ejecutas múltiples tests, por defecto estos se ejecutan en paralelo usando hilos, lo que significa que terminan de ejecutarse más rápido y obtienes feedback más rápido. Debido a que los tests se ejecutan al mismo tiempo, debes asegurarte que tus tests no dependan entre sí o de cualquier estado compartido, incluyendo un entorno compartido, como el directorio de trabajo actual o las variables de entorno.
Por ejemplo, digamos que cada uno de tus tests ejecuta código que crea un archivo en disco llamado test-output.txt y escribe algunos datos en ese archivo. Luego cada test lee los datos en ese archivo y aserta que el archivo contiene un valor particular, el cual es diferente en cada test. Debido a que los tests se ejecutan al mismo tiempo, un test podría sobreescribir el archivo en el tiempo entre que otro test escribe y lee el archivo. El segundo test fallará, no porque el código sea incorrecto, sino porque los tests han interferido entre sí mientras se ejecutaban en paralelo. Una solución es asegurarte que cada test escriba en un archivo diferente; otra solución es ejecutar los tests uno a la vez.
Si no deseas ejecutar los tests en paralelo o si deseas tener un control más
fino sobre el número de hilos usados, puedes enviar la bandera --test-threads
y el número de hilos que deseas usar al binario de test. Echa un vistazo al
siguiente ejemplo:
$ cargo test -- --test-threads=1
Establecemos el número de hilos de test a 1
, indicando al programa que no use
ningún paralelismo. Ejecutar los tests usando un hilo tomará más tiempo que
ejecutarlos en paralelo, pero los tests no interferirán entre sí si comparten
estado.
Mostrando el Output de las funciones
Por defecto, si un test pasa, la librería de tests de Rust captura cualquier
cosa impresa en la salida estándar. Por ejemplo, si llamamos a println!
en un
test y el test pasa, no veremos la salida de println!
en la terminal; solo
veremos la línea que indica que el test pasó. Si un test falla, veremos lo que
sea que se haya impreso en la salida estándar junto con el resto del mensaje de
falla.
Como ejemplo, el Listado 11-10 tiene una función tonta que imprime el valor de su parámetro y retorna 10, así como un test que pasa y un test que falla.
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {a}");
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
Cuando ejecutamos estos tests con cargo test
, vemos el siguiente output:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Nota que en ninguna parte de este output vemos I got the value 4
, que es lo
que se imprime cuando el test que pasa se ejecuta. Ese output ha sido capturado.
El output del test que falla, I got the value 8
, aparece en la sección del
resumen de tests, que también muestra la causa de la falla del test.
Si queremos ver los valores impresos por los tests que pasan también, podemos
decirle a Rust que muestre el output de los tests exitosos con --show-output
.
$ cargo test -- --show-output
Cuando ejecutamos los tests en el Listado 11-10 nuevamente con el flag
--show-output
, vemos el siguiente output:
$ cargo test -- --show-output
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
successes:
---- tests::this_test_will_pass stdout ----
I got the value 4
successes:
tests::this_test_will_pass
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Ejecutando un Subset de tests por nombre
A veces, ejecutar un conjunto completo de tests puede tomar mucho tiempo. Si
estás trabajando en código en un área particular, podrías querer ejecutar solo
los tests que pertenecen a ese código. Puedes elegir qué tests ejecutar
pasándole a cargo test
el nombre o nombres del test(s) que quieres ejecutar
como argumento.
Para demostrar cómo ejecutar un subset de tests, primero crearemos tres tests
para nuestra función add_two
, como se muestra en el Listado 11-11, y
elegiremos cuáles ejecutar.
pub fn add_two(a: usize) -> usize {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}
#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
Si ejecutamos los tests sin pasar ningún argumento, como vimos anteriormente, todos los tests se ejecutarán en paralelo:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Ejecutando un solo test
Podemos pasar el nombre de cualquier función de test a cargo test
para
ejecutar solo ese test:
$ cargo test one_hundred
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Solo se ejecutó el test con el nombre one_hundred
; los otros dos tests no
coincidieron con ese nombre. El output de los tests nos indica que tenemos más
tests que no se ejecutaron al mostrar 2 filtered out
al final.
No podemos especificar los nombres de varios tests de esta manera; solo se usará
el primer valor dado a cargo test
. Pero hay una manera de ejecutar varios
tests.
Filtrando para ejecutar múltiples tests
Podemos especificar parte de un nombre de test y cualquier test cuyo nombre
coincida con ese valor se ejecutará. Por ejemplo, como dos de nuestros tests
tienen add
en el nombre, podemos ejecutar esos dos ejecutando cargo test add
:
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
Este comando ejecutó todos los test con add
en el nombre y filtró el test
con el nombre one_hundred
. También nota que el módulo en el que aparece un
test se convierte en parte del nombre del test, por lo que podemos ejecutar
todos los tests en un módulo filtrando por el nombre del módulo.
Ignorando algunos tests a menos que se soliciten especificamente
A veces, algunos tests específicos pueden ser muy lentos para ejecutarse, por lo
que puede que quieras excluirlos en la mayoría de las ejecuciones de
cargo test
. En lugar de listar como argumentos todos los tests que quieres
ejecutar, puedes anotar los tests que consumen mucho tiempo usando el atributo
ignore
para excluirlos, como se muestra aquí:
Filename: src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
Después de #[test]
agregamos la línea #[ignore]
al test que queremos
excluir. Ahora cuando ejecutamos nuestros tests, it_works
se ejecuta, pero
expensive_test
no:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Esta función expensive_test
está listada como ignored
. Si queremos ejecutar
solo los tests ignorados, podemos usar cargo test -- -- ignored
:
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Controlando que tests se ejecutan, puedes asegurarte de que los resultados de
cargo test
serán retornados rápidamente. Cuando estés en un punto en el que
tenga sentido verificar los resultados de los tests ignorados y tengas tiempo
para esperar los resultados, puedes ejecutar cargo test -- --ignored
en su
lugar. Si quieres ejecutar todos los tests, ignorados o no, puedes ejecutar
cargo test -- --include-ignored
.