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.

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.

Filename: src/lib.rs

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!(10, value);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(5, value);
    }
}

Listing 11-10: Tests para una función que llama a println!

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: 5
 right: 10
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: 5
 right: 10
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.

Filename: src/lib.rs

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_two_and_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn add_three_and_two() {
        assert_eq!(5, add_two(3));
    }

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}

Listing 11-11: Tres tests con tres nombres diferentes

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

#[test]
fn it_works() {
    assert_eq!(2 + 2, 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 expensive_test ... ignored
test 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 rápidos. 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.