Cómo escribir pruebas

Beginner

This tutorial is from open-source community. Access the source code

Introducción

Bienvenido a Cómo escribir pruebas. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, aprenderemos a escribir pruebas en Rust utilizando atributos, macros y aserciones.

Cómo escribir pruebas

Las pruebas son funciones de Rust que verifican que el código no de prueba funcione de la manera esperada. Los cuerpos de las funciones de prueba generalmente realizan estas tres acciones:

  • Configurar cualquier dato o estado necesario.
  • Ejecutar el código que desea probar.
  • Asegurarse de que los resultados sean los que se esperan.

Echemos un vistazo a las características que Rust proporciona específicamente para escribir pruebas que realicen estas acciones, que incluyen el atributo test, algunas macros y el atributo should_panic.

La anatomía de una función de prueba

En su forma más simple, una prueba en Rust es una función que está anotada con el atributo test. Los atributos son metadatos sobre piezas de código de Rust; un ejemplo es el atributo derive que usamos con structs en el Capítulo 5. Para convertir una función en una función de prueba, agrega #[test] en la línea antes de fn. Cuando ejecutas tus pruebas con el comando cargo test, Rust construye un binario ejecutor de pruebas que ejecuta las funciones anotadas y reporta si cada función de prueba pasa o falla.

Cada vez que creamos un nuevo proyecto de biblioteca con Cargo, se genera automáticamente un módulo de pruebas con una función de prueba en él. Este módulo te da una plantilla para escribir tus pruebas para que no tengas que buscar la estructura y la sintaxis exactas cada vez que empiezas un nuevo proyecto. ¡Puedes agregar tantas funciones de prueba adicionales y tantos módulos de prueba como desees!

Vamos a explorar algunos aspectos de cómo funcionan las pruebas experimentando con la prueba de plantilla antes de probar realmente cualquier código. Luego escribiremos algunas pruebas del mundo real que llamen a algún código que hayamos escrito y aseguremos que su comportamiento sea correcto.

Vamos a crear un nuevo proyecto de biblioteca llamado adder que sumará dos números:

$ cargo new adder --lib
Created library $(adder) project
$ cd adder

El contenido del archivo src/lib.rs en tu biblioteca adder debería verse como en la Lista 11-1.

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
  1 #[test]
    fn it_works() {
        let result = 2 + 2;
      2 assert_eq!(result, 4);
    }
}

Lista 11-1: El módulo y la función de prueba generados automáticamente por cargo new

Por ahora, ignoremos las dos primeras líneas y centrémonos en la función. Observe la anotación #[test] [1]: este atributo indica que esta es una función de prueba, por lo que el ejecutor de pruebas sabe tratar esta función como una prueba. También podríamos tener funciones no de prueba en el módulo tests para ayudar a configurar escenarios comunes o realizar operaciones comunes, por lo que siempre necesitamos indicar cuáles funciones son pruebas.

El cuerpo del ejemplo de función utiliza la macro assert_eq! [2] para asegurar que result, que contiene el resultado de sumar 2 y 2, es igual a 4. Esta afirmación sirve como un ejemplo del formato para una prueba típica. Vamos a ejecutarlo para ver que esta prueba pasa.

El comando cargo test ejecuta todas las pruebas en nuestro proyecto, como se muestra en la Lista 11-2.

[object Object]

Lista 11-2: La salida de la ejecución de la prueba generada automáticamente

Cargo compiló y ejecutó la prueba. Vemos la línea running 1 test [1]. La siguiente línea muestra el nombre de la función de prueba generada, llamada it_works, y que el resultado de ejecutar esa prueba es ok [2]. El resumen general test result: ok. [3] significa que todas las pruebas pasaron, y la parte que dice 1 passed; 0 failed suma el número de pruebas que pasaron o fallaron.

Es posible marcar una prueba como ignorada para que no se ejecute en una instancia particular; lo cubriremos en "Ignorando algunas pruebas a menos que se solicite específicamente". Debido a que no lo hemos hecho aquí, el resumen muestra 0 ignored. También podemos pasar un argumento al comando cargo test para ejecutar solo las pruebas cuyo nombre coincida con una cadena; esto se llama filtrado y lo cubriremos en "Ejecutando un subconjunto de pruebas por nombre". Aquí no hemos filtrado las pruebas que se están ejecutando, por lo que el final del resumen muestra 0 filtered out.

La estadística 0 measured es para las pruebas de rendimiento que miden el rendimiento. Las pruebas de rendimiento, a la fecha de redacción de este documento, solo están disponibles en Rust nocturno. Consulte la documentación sobre las pruebas de rendimiento en https://doc.rust-lang.org/unstable-book/library-features/test.html para obtener más información.

La siguiente parte de la salida de la prueba que comienza en Doc-tests adder [4] es para los resultados de cualquier prueba de documentación. Todavía no tenemos ninguna prueba de documentación, pero Rust puede compilar cualquier ejemplo de código que aparezca en nuestra documentación de API. ¡Esta característica ayuda a mantener tus documentos y tu código actualizados! Discutiremos cómo escribir pruebas de documentación en "Comentarios de documentación como pruebas". Por ahora, ignoraremos la salida Doc-tests.

Vamos a empezar a personalizar la prueba a nuestras propias necesidades. Primero, cambia el nombre de la función it_works a un nombre diferente, como exploration, así:

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

Luego ejecuta cargo test nuevamente. La salida ahora muestra exploration en lugar de it_works:

running 1 test
test tests::exploration... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Ahora agregaremos otra prueba, pero esta vez haremos una prueba que falle. Las pruebas fallan cuando algo en la función de prueba se desborda. Cada prueba se ejecuta en un nuevo hilo, y cuando el hilo principal ve que un hilo de prueba ha muerto, la prueba se marca como fallida. En el Capítulo 9, hablamos de que la forma más simple de desbordarse es llamar a la macro panic!. Ingresa la nueva prueba como una función llamada another, para que tu archivo src/lib.rs se vea como la Lista 11-3.

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

Lista 11-3: Agregando una segunda prueba que fallará porque llamamos a la macro panic!

Ejecute las pruebas nuevamente usando cargo test. La salida debería verse como en la Lista 11-4, que muestra que nuestra prueba exploration pasó y another falló.

running 2 tests
test tests::exploration... ok
1 test tests::another... FAILED

2 failures:

---- tests::another stdout ----
thread'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

3 failures:
    tests::another

4 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'

Lista 11-4: Resultados de la prueba cuando una prueba pasa y una prueba falla

En lugar de ok, la línea test tests::another muestra FAILED [1]. Dos secciones nuevas aparecen entre los resultados individuales y el resumen: la primera [2] muestra la razón detallada de cada fallo de prueba. En este caso, obtenemos los detalles de que another falló porque se desbordó en 'Make this test fail' en la línea 10 del archivo src/lib.rs. La siguiente sección [3] lista solo los nombres de todas las pruebas que fallan, lo que es útil cuando hay muchas pruebas y mucha salida detallada de pruebas que fallan. Podemos usar el nombre de una prueba que falla para ejecutar solo esa prueba para depurarla más fácilmente; hablaremos más sobre maneras de ejecutar pruebas en "Controlar cómo se ejecutan las pruebas".

La línea de resumen se muestra al final [4]: en general, nuestro resultado de prueba es FAILED. Tuvimos una prueba que pasó y una prueba que falló.

Ahora que has visto cómo se ven los resultados de la prueba en diferentes escenarios, echemos un vistazo a algunas macros diferentes de panic! que son útiles en las pruebas.

Comprobando resultados con la macro assert!

La macro assert!, proporcionada por la biblioteca estándar, es útil cuando quieres asegurarte de que alguna condición en una prueba se evalúa como true. Le pasamos a la macro assert! un argumento que se evalúa como un booleano. Si el valor es true, nada pasa y la prueba pasa. Si el valor es false, la macro assert! llama a panic! para que la prueba falle. Usar la macro assert! nos ayuda a comprobar que nuestro código está funcionando de la manera que pretendemos.

En la Lista 5-15, usamos una struct Rectangle y un método can_hold, que se repiten aquí en la Lista 11-5. Vamos a poner este código en el archivo src/lib.rs, luego escribir algunas pruebas para él usando la macro assert!.

Nombre de archivo: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Lista 11-5: Usando la struct Rectangle y su método can_hold del Capítulo 5

El método can_hold devuelve un booleano, lo que significa que es un caso de uso perfecto para la macro assert!. En la Lista 11-6, escribimos una prueba que prueba el método can_hold creando una instancia de Rectangle que tiene un ancho de 8 y un alto de 7 y asegurándonos de que puede contener otra instancia de Rectangle que tiene un ancho de 5 y un alto de 1.

Nombre de archivo: src/lib.rs

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

    #[test]
  2 fn larger_can_hold_smaller() {
      3 let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

      4 assert!(larger.can_hold(&smaller));
    }
}

Lista 11-6: Una prueba para can_hold que comprueba si un rectángulo más grande puede contener en realidad un rectángulo más pequeño

Tenga en cuenta que hemos agregado una nueva línea dentro del módulo tests: use super::*; [1]. El módulo tests es un módulo regular que sigue las reglas de visibilidad habituales que cubrimos en "Rutas para referirse a un elemento en el árbol de módulos". Debido a que el módulo tests es un módulo interno, necesitamos traer el código que se está probando en el módulo externo al alcance del módulo interno. Usamos un glob aquí, por lo que cualquier cosa que definamos en el módulo externo está disponible para este módulo tests.

Hemos nombrado nuestra prueba larger_can_hold_smaller [2], y hemos creado las dos instancias de Rectangle que necesitamos [3]. Luego llamamos a la macro assert! y le pasamos el resultado de llamar a larger.can_hold(&smaller) [4]. Esta expresión debería devolver true, por lo que nuestra prueba debería pasar. ¡Vamos a averiguarlo!

running 1 test
test tests::larger_can_hold_smaller... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

¡Pasa! Vamos a agregar otra prueba, esta vez asegurándonos de que un rectángulo más pequeño no puede contener un rectángulo más grande:

Nombre de archivo: src/lib.rs

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

    #[test]
    fn larger_can_hold_smaller() {
        --snip--
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Debido a que el resultado correcto de la función can_hold en este caso es false, necesitamos negar ese resultado antes de pasarlo a la macro assert!. Como resultado, nuestra prueba pasará si can_hold devuelve false:

running 2 tests
test tests::larger_can_hold_smaller... ok
test tests::smaller_cannot_hold_larger... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

¡Dos pruebas que pasan! Ahora veamos qué pasa con nuestros resultados de prueba cuando introducimos un error en nuestro código. Cambiaremos la implementación del método can_hold reemplazando el signo mayor que por un signo menor que cuando compara los anchos:

--snip--

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

Ejecutar las pruebas ahora produce lo siguiente:

running 2 tests
test tests::smaller_cannot_hold_larger... ok
test tests::larger_can_hold_smaller... FAILED

failures:

---- tests::larger_can_hold_smaller stdout ----
thread'main' panicked at 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

¡Nuestras pruebas detectaron el error! Debido a que larger.width es 8 y smaller.width es 5, la comparación de los anchos en can_hold ahora devuelve false: 8 no es menor que 5.

Probando la igualdad con las macros assert_eq! y assert_ne!

Una forma común de verificar la funcionalidad es probar la igualdad entre el resultado del código que se está probando y el valor que esperas que devuelva el código. Podrías hacer esto usando la macro assert! y pasándole una expresión que use el operador ==. Sin embargo, esta es una prueba tan común que la biblioteca estándar proporciona un par de macros: assert_eq! y assert_ne!, para realizar esta prueba de manera más conveniente. Estas macros comparan dos argumentos para ver si son iguales o diferentes, respectivamente. También imprimirán los dos valores si la afirmación falla, lo que hace más fácil ver por qué la prueba falló; en cambio, la macro assert! solo indica que obtuvo un valor false para la expresión ==, sin imprimir los valores que dieron lugar al valor false.

En la Lista 11-7, escribimos una función llamada add_two que suma 2 a su parámetro, luego probamos esta función usando la macro assert_eq!.

Nombre de archivo: src/lib.rs

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

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

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

Lista 11-7: Probando la función add_two usando la macro assert_eq!

Veamos si pasa!

running 1 test
test tests::it_adds_two... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Le pasamos 4 como argumento a assert_eq!, que es igual al resultado de llamar a add_two(2). La línea para esta prueba es test tests::it_adds_two... ok, y el texto ok indica que nuestra prueba pasó.

Vamos a introducir un error en nuestro código para ver cómo se ve assert_eq! cuando falla. Cambia la implementación de la función add_two para que en lugar de sumar 2, sume 3:

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

Ejecuta las pruebas nuevamente:

running 1 test
test tests::it_adds_two... FAILED

failures:

---- tests::it_adds_two stdout ----
1 thread'main' panicked at 'assertion failed: `(left == right)`
2   left: `4`,
3  right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

¡Nuestra prueba detectó el error! La prueba it_adds_two falló, y el mensaje nos dice que la afirmación que falló fue assertion failed: (left == right)`\[1\] y cuáles son los valores deleft\[2\] yright\[3\]. Este mensaje nos ayuda a comenzar a depurar: el argumentoleftera4pero el argumentoright, donde teníamos add_two(2), era 5`. Puedes imaginar que esto sería especialmente útil cuando tenemos muchas pruebas en marcha.

Tenga en cuenta que en algunos lenguajes y marcos de prueba, los parámetros de las funciones de afirmación de igualdad se llaman expected y actual, y el orden en el que especificamos los argumentos importa. Sin embargo, en Rust, se llaman left y right, y el orden en el que especificamos el valor que esperamos y el valor que produce el código no importa. Podríamos escribir la afirmación en esta prueba como assert_eq!(add_two(2), 4), lo que resultaría en el mismo mensaje de error que muestra assertion failed: (left == right)``.

La macro assert_ne! pasará si los dos valores que le damos no son iguales y fallará si son iguales. Esta macro es más útil en casos en los que no estamos seguros de qué valor será, pero sabemos qué valor definitivamente no debería ser. Por ejemplo, si estamos probando una función que está garantizada de cambiar su entrada de alguna manera, pero la forma en que la entrada se cambia depende del día de la semana en el que ejecutamos nuestras pruebas, lo mejor que se puede afirmar es que la salida de la función no es igual a la entrada.

En el fondo, las macros assert_eq! y assert_ne! usan los operadores == y !=, respectivamente. Cuando las afirmaciones fallan, estas macros imprimen sus argumentos usando el formato de depuración, lo que significa que los valores que se están comparando deben implementar los tratos PartialEq y Debug. Todos los tipos primitivos y la mayoría de los tipos de la biblioteca estándar implementan estos tratos. Para los structs y los enums que defines tú mismo, necesitarás implementar PartialEq para afirmar la igualdad de esos tipos. También necesitarás implementar Debug para imprimir los valores cuando la afirmación falla. Debido a que ambos tratos son tratos derivables, como se mencionó en la Lista 5-12, esto por lo general es tan sencillo como agregar la anotación #[derive(PartialEq, Debug)] a la definición de tu struct o enum. Consulte el Apéndice C para obtener más detalles sobre estos y otros tratos derivables.

Agregando mensajes de error personalizados

También puedes agregar un mensaje personalizado para que se imprima con el mensaje de error como argumentos opcionales a las macros assert!, assert_eq! y assert_ne!. Cualquier argumento especificado después de los argumentos requeridos se pasa a la macro format! (discutida en "Concatenación con el operador + o la macro format!"), por lo que puedes pasar una cadena de formato que contenga marcadores de posición {} y valores para ir en esos marcadores de posición. Los mensajes personalizados son útiles para documentar lo que significa una afirmación; cuando una prueba falla, tendrás una mejor idea de cuál es el problema con el código.

Por ejemplo, supongamos que tenemos una función que saluda a las personas por nombre y queremos probar que el nombre que pasamos a la función aparezca en la salida:

Nombre de archivo: src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {name}!")
}

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

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

Los requisitos de este programa aún no han sido acordados, y estamos bastante seguros de que el texto Hello al principio del saludo cambiará. Decidimos que no queremos tener que actualizar la prueba cuando los requisitos cambien, por lo que en lugar de comprobar la igualdad exacta con el valor devuelto por la función greeting, simplemente afirmaremos que la salida contiene el texto del parámetro de entrada.

Ahora vamos a introducir un error en este código cambiando greeting para excluir name para ver cómo se ve el error de prueba predeterminado:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

Ejecutar esta prueba produce lo siguiente:

running 1 test
test tests::greeting_contains_name... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'assertion failed:
result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::greeting_contains_name

Este resultado solo indica que la afirmación falló y en qué línea está la afirmación. Un mensaje de error más útil imprimiría el valor de la función greeting. Vamos a agregar un mensaje de error personalizado compuesto por una cadena de formato con un marcador de posición lleno con el valor real que obtuvimos de la función greeting:

#[test]
fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
        result.contains("Carol"),
        "Greeting did not contain name, value was `{result}`"
    );
}

Ahora cuando ejecutamos la prueba, obtendremos un mensaje de error más informativo:

---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'Greeting did not contain name, value
was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

Podemos ver el valor que realmente obtuvimos en la salida de la prueba, lo que nos ayudaría a depurar lo que sucedió en lugar de lo que esperábamos que sucediera.

Comprobando desbordamientos con should_panic

Además de comprobar los valores de retorno, es importante comprobar que nuestro código maneje las condiciones de error como esperamos. Por ejemplo, considere el tipo Guess que creamos en la Lista 9-13. Otro código que utiliza Guess depende de la garantía de que las instancias de Guess contendrán solo valores entre 1 y 100. Podemos escribir una prueba que asegure que intentar crear una instancia de Guess con un valor fuera de ese rango provoque un desbordamiento.

Hacemos esto agregando el atributo should_panic a nuestra función de prueba. La prueba pasa si el código dentro de la función se desborda; la prueba falla si el código dentro de la función no se desborda.

La Lista 11-8 muestra una prueba que comprueba que las condiciones de error de Guess::new ocurran cuando esperamos que ocurran.

// src/lib.rs
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Lista 11-8: Probando que una condición causará un desbordamiento

Colocamos el atributo #[should_panic] después del atributo #[test] y antes de la función de prueba a la que se aplica. Echemos un vistazo al resultado cuando esta prueba pasa:

running 1 test
test tests::greater_than_100 - should panic... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

¡Parece bien! Ahora vamos a introducir un error en nuestro código eliminando la condición de que la función new se desborde si el valor es mayor que 100:

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

Cuando ejecutamos la prueba de la Lista 11-8, fallará:

running 1 test
test tests::greater_than_100 - should panic... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

En este caso no obtenemos un mensaje muy útil, pero cuando miramos la función de prueba, vemos que está anotada con #[should_panic]. El fallo que obtuvimos significa que el código en la función de prueba no causó un desbordamiento.

Las pruebas que usan should_panic pueden ser imprecisas. Una prueba should_panic pasará incluso si la prueba se desborda por un motivo diferente al que esperábamos. Para hacer que las pruebas should_panic sean más precisas, podemos agregar un parámetro opcional expected al atributo should_panic. El harness de pruebas se asegurará de que el mensaje de error contenga el texto proporcionado. Por ejemplo, considere el código modificado de Guess en la Lista 11-9 donde la función new se desborda con mensajes diferentes dependiendo de si el valor es demasiado pequeño o demasiado grande.

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Lista 11-9: Probando un desbordamiento con un mensaje de desbordamiento que contiene una subcadena especificada

Esta prueba pasará porque el valor que ponemos en el parámetro expected del atributo should_panic es una subcadena del mensaje con el que se desborda la función Guess::new. Podríamos haber especificado el mensaje de desbordamiento completo que esperamos, que en este caso sería Guess value must be less than or equal to 100, got 200. Lo que elijas especificar depende de cuánto del mensaje de desbordamiento es único o dinámico y de qué tan precisa quieres que sea tu prueba. En este caso, una subcadena del mensaje de desbordamiento es suficiente para asegurar que el código en la función de prueba ejecuta el caso else if value > 100.

Para ver lo que sucede cuando una prueba should_panic con un mensaje expected falla, vamos a introducir nuevamente un error en nuestro código intercambiando los cuerpos de los bloques if value < 1 y else if value > 100:

// src/lib.rs
--snip--
if value < 1 {
    panic!(
        "Guess value must be less than or equal to 100, got {}.",
        value
    );
} else if value > 100 {
    panic!(
        "Guess value must be greater than or equal to 1, got {}.",
        value
    );
}
--snip--

Esta vez cuando ejecutamos la prueba should_panic, fallará:

running 1 test
test tests::greater_than_100 - should panic... FAILED

failures:

---- tests::greater_than_100 stdout ----
thread'main' panicked at 'Guess value must be greater than or equal to 1, got
200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: `"Guess value must be greater than or equal to 1, got
200."`,
 expected substring: `"less than or equal to 100"`

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s

El mensaje de error indica que esta prueba sí se desbordó como esperábamos, pero el mensaje de desbordamiento no incluyó la cadena esperada 'Guess value must be less than or equal to 100'. El mensaje de desbordamiento que obtuvimos en este caso fue Guess value must be greater than or equal to 1, got 200. Ahora podemos empezar a averiguar dónde está nuestro error.

Usando Result<T, E> en las pruebas

Hasta ahora, todas nuestras pruebas se desbordan cuando fallan. ¡También podemos escribir pruebas que usen Result<T, E>! Aquí está la prueba de la Lista 11-1, reescrita para usar Result<T, E> y devolver un Err en lugar de desbordarse:

Nombre de archivo: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

La función it_works ahora tiene el tipo de retorno Result<(), String>. En el cuerpo de la función, en lugar de llamar a la macro assert_eq!, devolvemos Ok(()) cuando la prueba pasa y un Err con una String dentro cuando la prueba falla.

Escribir pruebas para que devuelvan un Result<T, E> te permite usar el operador de interrogación en el cuerpo de las pruebas, lo que puede ser una forma conveniente de escribir pruebas que deben fallar si cualquier operación dentro de ellas devuelve una variante Err.

No puedes usar la anotación #[should_panic] en pruebas que usan Result<T, E>. Para afirmar que una operación devuelve una variante Err, no uses el operador de interrogación en el valor Result<T, E>. En su lugar, usa assert!(value.is_err()).

Ahora que conoces varias maneras de escribir pruebas, echemos un vistazo a lo que está sucediendo cuando ejecutamos nuestras pruebas y exploremos las diferentes opciones que podemos usar con cargo test.

Resumen

¡Felicidades! Has completado el laboratorio de Cómo Escribir Pruebas. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.