Функциональность библиотеки на Rust с использованием тестирования на основе поведения

RustRustBeginner
Практиковаться сейчас

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

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Разработка функциональности библиотеки с использованием тестируемого разработки. Этот лаба является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этом лабе мы разработаем функциональность библиотеки с использованием тестируемого разработки, чтобы добавить логику поиска в программу.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) 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{{"Функциональность библиотеки на Rust с использованием тестирования на основе поведения"}} rust/mutable_variables -.-> lab-100421{{"Функциональность библиотеки на Rust с использованием тестирования на основе поведения"}} rust/for_loop -.-> lab-100421{{"Функциональность библиотеки на Rust с использованием тестирования на основе поведения"}} rust/function_syntax -.-> lab-100421{{"Функциональность библиотеки на Rust с использованием тестирования на основе поведения"}} rust/expressions_statements -.-> lab-100421{{"Функциональность библиотеки на Rust с использованием тестирования на основе поведения"}} rust/method_syntax -.-> lab-100421{{"Функциональность библиотеки на Rust с использованием тестирования на основе поведения"}} rust/operator_overloading -.-> lab-100421{{"Функциональность библиотеки на Rust с использованием тестирования на основе поведения"}} end

Тестируемое разработка

Теперь, когда мы извлекли логику в src/lib.rs и оставили сбор аргументов и обработку ошибок в src/main.rs, гораздо проще писать тесты для ядра функциональности нашего кода. Мы можем напрямую вызывать функции с различными аргументами и проверять возвращаемые значения, не нужно вызывать нашу бинарную программу из командной строки.

В этом разделе мы добавим логику поиска в программу minigrep с использованием процесса тестируемого разработки (TDD) с такими шагами:

  1. Напишите тест, который не проходит, и запустите его, чтобы убедиться, что он не проходит по той причине, которую вы ожидаете.
  2. Напишите или измените только столько кода, чтобы новый тест прошел.
  3. Перестроите код, который вы только что добавили или изменили, и убедитесь, что тесты продолжают проходить.
  4. Повторяйте с шага 1!

Хотя это всего лишь один из многих способов написания программного обеспечения, TDD может помочь в проектировании кода. Написание теста перед написанием кода, который делает тест проходящим, помогает поддерживать высокую степень покрытия тестами на протяжении всего процесса.

Мы проведем тестируемое развитие реализации функциональности, которая на самом деле будет искать строку запроса в содержимом файла и генерировать список строк, которые соответствуют запросу. Мы добавим эту функциональность в функцию под названием search.

Написание не проходящего теста

Поскольку мы больше не нуждаемся в них, удалим println! инструкции из src/lib.rs и src/main.rs, которые мы использовали для проверки поведения программы. Затем в src/lib.rs мы добавим модуль tests с тестовой функцией, как мы делали в главе 11. Тестовая функция определяет поведение, которое мы хотим, чтобы функция search имела: она будет принимать запрос и текст для поиска, и возвращать только те строки из текста, которые содержат запрос. В листинге 12-15 показан этот тест, который еще не скомпилируется.

Имя файла: 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)
        );
    }
}

Листинг 12-15: Создание не проходящего теста для функции search, которую мы хотели бы иметь

Этот тест ищет строку "duct". Текст, по которому мы ищем, состоит из трех строк, только одна из которых содержит "duct" (заметьте, что обратный слэш после открытой двойной кавычки говорит Rust не вставлять символ новой строки в начале содержимого этого литерала строки). Мы утверждаем, что значение, возвращаемое функцией search, содержит только ту строку, которую мы ожидаем.

Мы еще не можем запустить этот тест и увидеть, как он не проходит, потому что тест даже не компилируется: функция search еще не существует! Согласно принципам TDD, мы добавим столько кода, чтобы тест скомпилировался и запустился, добавив определение функции search, которая всегда возвращает пустой вектор, как показано в листинге 12-16. Затем тест должен скомпилироваться и не пройти, потому что пустой вектор не совпадает с вектором, содержащим строку "safe, fast, productive.".

Имя файла: src/lib.rs

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

Листинг 12-16: Определение достаточного количества функции search, чтобы наш тест скомпилировался

Обратите внимание, что нам нужно определить явный срок жизни 'a в сигнатуре функции search и использовать этот срок жизни с аргументом contents и возвращаемым значением. Напомним, что в главе 10 параметры сроков жизни определяют, какой аргументский срок жизни связан с сроком жизни возвращаемого значения. В этом случае мы указываем, что возвращаемый вектор должен содержать срезы строк, которые ссылаются на срезы аргумента contents (а не аргумента query).

Другими словами, мы говорим Rust, что данные, возвращаемые функцией search, будут жить столько же времени, сколько и данные, переданные в функцию search в аргументе contents. Это важно! Данные, на которые ссылается срез, должны быть валидными, чтобы ссылка была валидной; если компилятор предполагает, что мы создаем срезы строк из query вместо contents, он будет неправильно выполнять свою проверку безопасности.

Если мы забываем аннотации сроков жизни и пытаемся скомпилировать эту функцию, мы получим эту ошибку:

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 не может определить, какой из двух аргументов мы нуждаемся в, поэтому мы должны явно его указать. Поскольку contents является аргументом, содержащим весь наш текст, и мы хотим вернуть части этого текста, которые совпадают, мы знаем, что contents является аргументом, который должен быть связан с возвращаемым значением с использованием синтаксиса срока жизни.

В других языках программирования не требуется связывать аргументы с возвращаемыми значениями в сигнатуре, но с течением времени это будет становиться проще. Вы можете сравнить этот пример с примерами в разделе "Проверка ссылок с использованием сроков жизни".

Теперь давайте запустим тест:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 0.97s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result... FAILED

failures:

---- tests::one_result stdout ----
thread 'tests::one_result' panicked at 'assertion failed: `(left == right)`
  left: `["safe, fast, productive."]`,
 right: `[]`', src/lib.rs:47:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::one_result

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

error: test failed, to rerun pass '--lib'

Отлично, тест не проходит, именно так, как мы ожидали. Давайте сделаем тест проходящим!

Написание кода для прохождения теста

В настоящее время наш тест не проходит, потому что мы всегда возвращаем пустой вектор. Чтобы исправить это и реализовать search, наша программа должна выполнить следующие шаги:

  1. Пройтись по каждой строке содержимого.
  2. Проверить, содержит ли строка нашу строку запроса.
  3. Если да, добавить ее в список возвращаемых значений.
  4. Если нет, ничего не делать.
  5. Вернуть список совпадающих результатов.

Давайте пройдемся по каждому шагу, начиная с итерации по строкам.

Итерация по строкам с помощью метода lines

В Rust есть полезный метод для обработки итерации по строкам по одной, удобно названный lines, который работает, как показано в листинге 12-17. Обратите внимание, что это еще не скомпилируется.

Имя файла: src/lib.rs

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

Листинг 12-17: Итерация по каждой строке в contents

Метод lines возвращает итератор. Мы поговорим о итераторах более подробно в главе 13, но вспомните, что вы видели этот способ использования итератора в листинге 3-5, где мы использовали цикл for с итератором, чтобы выполнить некоторый код для каждого элемента в коллекции.

Поиск запроса в каждой строке

Далее мы проверим, содержит ли текущая строка нашу строку запроса. К счастью, у строк есть полезный метод под названием contains, который делает это за нас! Добавьте вызов метода contains в функцию search, как показано в листинге 12-18. Обратите внимание, что это по-прежнему не скомпилируется.

Имя файла: src/lib.rs

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

Листинг 12-18: Добавление функциональности для проверки, содержит ли строка строку из query

В данный момент мы накапливаем функциональность. Чтобы код скомпилировался, нам нужно вернуть значение из тела функции, как мы указали в сигнатуре функции.

Сохранение совпадающих строк

Для завершения этой функции нам нужно способ хранения совпадающих строк, которые мы хотим вернуть. Для этого мы можем создать изменяемый вектор перед циклом for и вызвать метод push, чтобы сохранить line в векторе. После цикла for мы возвращаем вектор, как показано в листинге 12-19.

Имя файла: 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
}

Листинг 12-19: Сохранение строк, которые совпадают, чтобы мы могли их вернуть

Теперь функция search должна возвращать только те строки, которые содержат query, и наш тест должен пройти. Запустим тест:

$ 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

Наш тест прошел, поэтому мы знаем, что это работает!

На этом этапе мы можем рассмотреть возможности для рефакторинга реализации функции поиска, сохраняя при этом проход тестов для поддержания той же функциональности. Код в функции поиска не太差, но он не использует некоторые полезные функции итераторов. Мы вернемся к этому примеру в главе 13, где мы подробно изучим итераторы и рассмотрим, как его улучшить.

Теперь, когда функция search работает и протестирована, нам нужно вызвать search из нашей функции run. Мы должны передать значение config.query и contents, которое run читает из файла, функции search. Затем run выведет каждую строку, возвращаемую функцией search:

Имя файла: 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(())
}

Мы по-прежнему используем цикл for, чтобы вернуть каждую строку из search и вывести ее.

Теперь вся программа должна работать! Попробуем ее, сначала с словом, которое должно вернуть ровно одну строку из стихотворения Эмили Диккенсон: 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

Отлично! Теперь попробуем слово, которое совпадет с несколькими строками, например, 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!

И наконец, давайте убедимся, что мы не получаем никаких строк, когда ищем слово, которое не встречается нигде в стихотворении, например, monomorphization:

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

Отлично! Мы создали свою мини-версию классического инструмента и узнали много о том, как структурировать приложения. Мы также узнали немного о вводе-выводе файлов, жизненных периодах, тестировании и разборе командной строки.

Чтобы завершить этот проект, мы кратко покажем, как работать с переменными окружения и как выводить сообщения в стандартный вывод ошибок, что полезно при написании командных строковых программ.

Резюме

Поздравляем! Вы завершили лабораторную работу "Разработка функциональности библиотеки с использованием тестирования на основе поведения". Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.