Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas

RustRustBeginner
Practicar Ahora

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

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Desarrollando la funcionalidad de la biblioteca con Desarrollo Guiado por Pruebas. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, desarrollaremos la funcionalidad de la biblioteca utilizando el desarrollo guiado por pruebas para agregar lógica de búsqueda al programa.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100421{{"Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas"}} rust/mutable_variables -.-> lab-100421{{"Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas"}} rust/for_loop -.-> lab-100421{{"Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas"}} rust/function_syntax -.-> lab-100421{{"Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas"}} rust/expressions_statements -.-> lab-100421{{"Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas"}} rust/method_syntax -.-> lab-100421{{"Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas"}} rust/operator_overloading -.-> lab-100421{{"Funcionalidad de la biblioteca de Rust con Desarrollo Guiado por Pruebas"}} end

Desarrollo guiado por pruebas

Ahora que hemos extraído la lógica en src/lib.rs y hemos dejado la recopilación de argumentos y el manejo de errores en src/main.rs, es mucho más fácil escribir pruebas para la funcionalidad central de nuestro código. Podemos llamar directamente a las funciones con varios argumentos y comprobar los valores de retorno sin tener que llamar a nuestro binario desde la línea de comandos.

En esta sección, agregaremos la lógica de búsqueda al programa minigrep utilizando el proceso de desarrollo guiado por pruebas (TDD) con los siguientes pasos:

  1. Escribe una prueba que falle y ejecútala para asegurarte de que falle por la razón que esperas.
  2. Escribe o modifica solo el código suficiente para que la nueva prueba pase.
  3. Refactoriza el código que acabas de agregar o cambiar y asegúrate de que las pruebas sigan pasando.
  4. ¡Repite desde el paso 1!

Aunque es solo una de las muchas maneras de escribir software, el TDD puede ayudar a impulsar el diseño del código. Escribir la prueba antes de escribir el código que hace que la prueba pase ayuda a mantener una alta cobertura de pruebas en todo el proceso.

Vamos a probar y desarrollar la implementación de la funcionalidad que realmente buscará la cadena de consulta en el contenido del archivo y producirá una lista de líneas que coincidan con la consulta. Agregaremos esta funcionalidad en una función llamada search.

Escribiendo una prueba que falla

Como ya no los necesitamos, eliminemos las declaraciones println! de src/lib.rs y src/main.rs que usamos para comprobar el comportamiento del programa. Luego, en src/lib.rs, agregaremos un módulo tests con una función de prueba, como hicimos en el Capítulo 11. La función de prueba especifica el comportamiento que queremos que tenga la función search: tomará una consulta y el texto a buscar, y devolverá solo las líneas del texto que contengan la consulta. La Lista 12-15 muestra esta prueba, que aún no se compilará.

Nombre del archivo: src/lib.rs

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(
            vec!["safe, fast, productive."],
            search(query, contents)
        );
    }
}

Lista 12-15: Creando una prueba que falla para la función search que deseamos tener

Esta prueba busca la cadena "duct". El texto que estamos buscando tiene tres líneas, solo una de las cuales contiene "duct" (ten en cuenta que la barra invertida después de la comilla doble de apertura le dice a Rust que no coloque un carácter de nueva línea al principio del contenido de este literal de cadena). Assertivamos que el valor devuelto por la función search contiene solo la línea que esperamos.

Todavía no podemos ejecutar esta prueba y verla fallar porque la prueba ni siquiera se compila: ¡la función search aún no existe! De acuerdo con los principios del TDD, agregaremos solo el código suficiente para que la prueba se compile y se ejecute agregando una definición de la función search que siempre devuelva un vector vacío, como se muestra en la Lista 12-16. Entonces, la prueba debería compilarse y fallar porque un vector vacío no coincide con un vector que contiene la línea "safe, fast, productive.".

Nombre del archivo: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    vec![]
}

Lista 12-16: Definiendo solo lo suficiente de la función search para que nuestra prueba se compile

Tenga en cuenta que necesitamos definir una vida explícita 'a en la firma de search y usar esa vida con el argumento contents y el valor de retorno. Recuerde en el Capítulo 10 que los parámetros de vida especifican qué vida del argumento está conectada a la vida del valor de retorno. En este caso, indicamos que el vector devuelto debe contener rebanadas de cadena que hagan referencia a rebanadas del argumento contents (en lugar del argumento query).

En otras palabras, le decimos a Rust que los datos devueltos por la función search vivirán durante tanto tiempo como los datos pasados a la función search en el argumento contents. ¡Esto es importante! Los datos referenciados por una rebanada deben ser válidos para que la referencia sea válida; si el compilador asume que estamos haciendo rebanadas de cadena de query en lugar de contents, hará su comprobación de seguridad incorrectamente.

Si olvidamos las anotaciones de vida y tratamos de compilar esta función, obtendremos este error:

error[E0106]: missing lifetime specifier
  --> src/lib.rs:31:10
   |
29 |     query: &str,
   |            ----
30 |     contents: &str,
   |               ----
31 | ) -> Vec<&str> {
   |          ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
   |
28 ~ pub fn search<'a>(
29 ~     query: &'a str,
30 ~     contents: &'a str,
31 ~ ) -> Vec<&'a str> {
   |

Rust no puede posiblemente saber cuál de los dos argumentos necesitamos, por lo que debemos decirlo explícitamente. Debido a que contents es el argumento que contiene todo nuestro texto y queremos devolver las partes de ese texto que coinciden, sabemos que contents es el argumento que debe estar conectado al valor de retorno usando la sintaxis de vida.

Otros lenguajes de programación no requieren que conecten los argumentos con los valores de retorno en la firma, pero esta práctica se tornará más fácil con el tiempo. Puede que desee comparar este ejemplo con los ejemplos en "Validando Referencias con Lifetimes".

Ahora ejecutemos la prueba:

[object Object]

Genial, la prueba falla, exactamente como esperábamos. ¡Vamos a hacer que la prueba pase!

Escribiendo código para que la prueba pase

Actualmente, nuestra prueba está fallando porque siempre devolvemos un vector vacío. Para solucionar eso e implementar search, nuestro programa debe seguir estos pasos:

  1. Itera a través de cada línea del contenido.
  2. Verifica si la línea contiene la cadena de consulta.
  3. Si es así, agréguela a la lista de valores que estamos devolviendo.
  4. Si no es así, no hagas nada.
  5. Devuelve la lista de resultados que coinciden.

Vamos a trabajar en cada paso, comenzando por iterar a través de las líneas.

Iterando a través de líneas con el método lines

Rust tiene un método útil para manejar la iteración línea por línea de cadenas, convenientemente nombrado lines, que funciona como se muestra en la Lista 12-17. Tenga en cuenta que esto aún no se compilará.

Nombre del archivo: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    for line in contents.lines() {
        // hacer algo con line
    }
}

Lista 12-17: Iterando a través de cada línea en contents

El método lines devuelve un iterador. Hablaremos de los iteradores en profundidad en el Capítulo 13, pero recuerde que vio esta forma de usar un iterador en la Lista 3-5, donde usamos un bucle for con un iterador para ejecutar un código en cada elemento de una colección.

Buscando la consulta en cada línea

A continuación, comprobaremos si la línea actual contiene la cadena de consulta. Afortunadamente, las cadenas tienen un método útil llamado contains que lo hace por nosotros. Agregue una llamada al método contains en la función search, como se muestra en la Lista 12-18. Tenga en cuenta que esto todavía no se compilará.

Nombre del archivo: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // hacer algo con line
        }
    }
}

Lista 12-18: Agregando funcionalidad para ver si la línea contiene la cadena en query

En este momento, estamos construyendo funcionalidad. Para que el código se compile, necesitamos devolver un valor del cuerpo como indicamos que haríamos en la firma de la función.

Almacenando las líneas que coinciden

Para terminar esta función, necesitamos una forma de almacenar las líneas que coinciden y que queremos devolver. Para eso, podemos crear un vector mutable antes del bucle for y llamar al método push para almacenar una line en el vector. Después del bucle for, devolvemos el vector, como se muestra en la Lista 12-19.

Nombre del archivo: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

Lista 12-19: Almacenando las líneas que coinciden para poder devolverlas

Ahora, la función search debería devolver solo las líneas que contienen query, y nuestra prueba debería pasar. Vamos a ejecutar la prueba:

$ cargo test
--snip--
running 1 test
test tests::one_result... ok

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

Nuestra prueba pasó, ¡por lo que sabemos que funciona!

En este punto, podríamos considerar oportunidades para refactorizar la implementación de la función de búsqueda mientras mantenemos las pruebas pasando para mantener la misma funcionalidad. El código en la función de búsqueda no está demasiado mal, pero no aprovecha algunas características útiles de los iteradores. Volveremos a este ejemplo en el Capítulo 13, donde exploraremos en detalle los iteradores y veremos cómo mejorarlo.

Ahora que la función search está funcionando y probada, necesitamos llamar a search desde nuestra función run. Necesitamos pasar el valor config.query y el contents que run lee del archivo a la función search. Luego, run imprimirá cada línea devuelta por search:

Nombre del archivo: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

Todavía estamos usando un bucle for para devolver cada línea de search e imprimirla.

Ahora todo el programa debería funcionar. Probémoslo, primero con una palabra que debería devolver exactamente una línea del poema de Emily Dickinson: frog.

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

Genial! Ahora probemos una palabra que coincida con múltiples líneas, como body:

$ cargo run -- body poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

Y finalmente, asegúrense de que no obtenemos ninguna línea cuando buscamos una palabra que no está en ningún lugar del poema, como monomorphization:

$ cargo run -- monomorphization poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

Excelente! Hemos construido nuestra propia versión mini de una herramienta clásica y hemos aprendido mucho sobre cómo estructurar aplicaciones. También hemos aprendido un poco sobre la entrada y salida de archivos, los lifetimes, las pruebas y el análisis de la línea de comandos.

Para terminar este proyecto, demostraremos brevemente cómo trabajar con variables de entorno y cómo imprimir en el error estándar, ambos son útiles cuando se escribe un programa de línea de comandos.

Resumen

¡Felicitaciones! Has completado el laboratorio de Desarrollo de la Funcionalidad de la Biblioteca con Desarrollo Guiado por Pruebas. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.