Разработка функционала библиотеки с помощью методологии "разработка через тестирование"

Test Driven Development (TDD) - "разработка через тестирование".

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

В этой секции Главы 12 мы добавим функционал поиска в пакет minigrep с помощью TDD. Эта методология имеет несколько шагов:

  • Написать тест, которые не работает. Проверяем это.
  • Изменить тест, который будет работать. Проверяем это.
  • Изменяем код, который мы тестировали и проверяем, что он работает.
  • Повторим.

Эта методология одни из возможных методик разработки программ. Отличительной особенностью TDD является помощь в разработке структуры кода. Написание теста прежде написания кода даёт возможность реализовать высокую степень покрытия тестами разрабатываемый функционал. Т.е. помогает проверить возможные режимы её работы.

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

Написание теста с ошибкой

Первое, удалим макрос println! из файлов src/lib.rs иd src/main.rs. Далее, добавим в модуль test с функции тестирования (так как мы это делали в Главе 11). Тестовая функция определяет поведение, наподобие функции search: мы получаем текст запроса и возвращаем только строки текста, где искомый текст был найден. Код программы показывает реализацию описанного функционала (12-15)::

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
#      vec![]
# }
#
#[cfg(test)]
mod test {
    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” в строк состоящей из 3-х строк. Только в одной из них есть текст “duct”. Мы проверяем возвращаемое значение функции search с ожидаемой строкой.

Пока мы не можем запустить тест и посмотреть на этот ошибочный результат. Этот код не будет скомпилирован, т.к. функции search ещё не существует. Добавим определение функции search. Данная функция будет возвращать пустой вектор (код 12-16). После наш код сможет быть скомпилирован и тест будет выдавать ошибку, т.к. пустой вектор не равен вектору с данными.

Filename: src/lib.rs

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

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

Обратите внимание, что нам надо явно указать время жизни переменной 'a contents и возвращаемого значения Vec<&'a str>. Напомним (Глава 10), что время жизни переменных связывает один из входных параметров с выходным. В нашем случае мы сообщаем компилятору, что срез текстовых данных переменной contents и данные вектора выходных данных будут ссылаться на одни и туже строку.

Если вы попытаетесь скомпилировать код программ без использования переменных времени жизни - вы получите сообщение об ошибке:

error[E0106]: missing lifetime specifier
 --> src/lib.rs:5:47
  |
5 | fn search(query: &str, contents: &str) -> Vec<&str> {
  |                                               ^ expected 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`

Компилятор без указания этих данных не могут быть уверенным в отсутствии некоренных ссылочных данных. Т.к. аргумент contents содержит текст и мы собираемся возвратить часть данного текста мы можем установить связь между этим аргументом и выходными данными.

Другие языки программирования не требуют организовать явную связь подобного рода. Такое избыточное и подробное описание может показаться странным, но эта избыточность оправдывается корректностью работы кода.

Проверим наш тест ещё раз:

$ cargo test
...warnings...
    Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs
     Running target/debug/deps/minigrep-abcabcabc

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

failures:

---- test::one_result stdout ----
    thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.


failures:
    test::one_result

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured

error: test failed

Отлично. Наш тест не сработал. Это мы и ожидали получить. Продолжим!

Написание кода, которые поможет тесту добиться искомого результата

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

  • Проанализировать про всеем строкам текста на наличие нужного текста.
  • Если он есть, добавить структур в вектор.
  • Если нет, выбрать новую строку.
  • Возвратить результат.

Проделаем эти действия поэтапно.

Выборка строк с помощью метода lines

Стандартная библиотека предоставляет метод выборки строк одна за одной. Код 12-17:

Filename: 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-6), где использовался цикл for.

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

Далее. мы добавим функционал поиска необходимого текста в строке. Есть метод contains, который делает всё работу. Добавим вызов этого метода (12-18):

Filename: 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: Добавление функционала проверки наличия подстроки в тексте

Сохранение найденной строки

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

Filename: 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 может возвратить искомые данные и тест может быть пройден:

$ cargo test
running 1 test
test test::one_result ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

Отличная работа! Тест пройден.

Теперь мы можем приступить к рефакторингу кода. В нем мы пока не использовали возможности итераторов. Мы вернёмся к этому коду в Главе 13 и внесём исправления.

Использование функции search в функции run

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

Filename: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<Error>> {
    let mut f = File::open(config.filename)?;

    let mut contents = String::new();
    f.read_to_string(&mut contents)?;

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

    Ok(())
}

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

Проверим работу программы после рефакторинга:

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

Отлично! Теперь введём другой текст, например, “the”:

$ cargo run the poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/minigrep the poem.txt`
Then there’s a pair of us — don’t tell!
To tell your name the livelong day

Теперь проверим отсутствующего слова “monomorphization”:

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

Отлично! Мы реализовали сокращенную версию функции grep и научились структурировать приложение. Мы также изучили о чтении данных из файла, повторили использование переменных времени жизни, тестировали код и использовали команды cargo.

Далее мы рассмотрим, как можнго работать с переменными окружения системы и как использовать поток ошибок.