Материал: Язык программирования Rust

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

Написание автоматизированных тестов
В своём эссе 1972 года “The Humble Programmer,” Edsger W. Dijkstra сказал, что
«Тестирование программы может быть очень эффективным способом показать наличие ошибок, но это безнадёжно неадекватно для показа их отсутствия». Это не значит, что мы не должны пытаться тестировать столько, сколько мы можем!
Корректностью программы считается то, в какой степени наш код выполняет именно то,
что мы задумывали. Rust разработан с учётом большой озабоченности корректностью программ, но корректность сложна и нелегко доказуема. Система типизации Rust берет на себя огромную часть этого бремени, но она не может уловить абсолютно все проблемы. Поэтому в Rust предусмотрена возможность написания автотестов.
Допустим, мы пишем функцию add_two
, которая прибавляет 2 к любому переданному ей числу. Сигнатура этой функции принимает целое число в качестве параметра и возвращает целое число в качестве результата. Когда мы реализуем и компилируем эту функцию, Rust выполняет всю проверку типов и проверку заимствований, которую вы уже изучили, чтобы убедиться, что, например, мы не передаём значение
String или недопустимую ссылку в эту функцию. Но Rust не способен проверить, что эта функция сделает именно то, что мы задумали, то есть вернёт параметр плюс 2, а не, скажем,
параметр плюс 10 или параметр минус 50! Вот тут-то и приходят на помощь тесты.
Мы можем написать тесты, которые утверждают, например, что когда мы передаём
3
в функцию add_two
, возвращаемое значение будет
5
. Мы можем запускать эти тесты всякий раз, когда мы вносим изменения в наш код, чтобы убедиться, что любое существующее правильное поведение не изменилось.
Тестирование - сложный навык: мы не сможем охватить все детали написания хороших тестов в одной главе, но мы обсудим основные подходы к тестированию в Rust. Мы поговорим об аннотациях и макросах, доступных вам для написания тестов, о поведении по умолчанию и параметрах, предусмотренных для запуска тестов, а также о том, как организовать тесты в модульные тесты и интеграционные тесты.

Как писать тесты
Тесты - это функции Rust, которые проверяют, что не тестовый код работает ожидаемым образом. Содержимое тестовых функций обычно выполняет следующие три действия:
1. Установка любых необходимых данных или состояния.
2. Запуск кода, который вы хотите проверить.
3. Утверждение, что результаты являются теми, которые вы ожидаете.
Давайте рассмотрим функции предоставляемые в Rust специально для написания тестов,
которые выполнят все эти действия, включая атрибут test
, несколько макросов и атрибут should_panic
Структура тестирующей функции
В простейшем случае в Rust тест - это функция, аннотированная атрибутом test
Атрибуты представляют собой метаданные о фрагментах кода Rust; один из примеров атрибут derive
, который мы использовали со структурами в главе 5. Чтобы изменить функцию в тестирующую функцию добавьте
#[test]
в строку перед fn
. Когда вы запускаете тесты командой cargo test
, Rust создаёт бинарный модуль выполняющий функции аннотированные атрибутом test и сообщающий о том, прошла успешно или не прошла каждая тестирующая функция.
Когда мы создаём новый проект библиотеки с помощью Cargo, то в нём автоматически генерируется тестовый модуль с тест функцией для нас. Этот модуль поможет вам начать написание ваших тестов, так что вам не нужно искать точную структуру и синтаксис тестовых функций каждый раз, когда вы начинаете новый проект. Вы можете добавить как большее количество дополнительных тестовых функций так и несколько тестовых модулей!
Мы исследуем некоторые аспекты работы тестов, экспериментируя с шаблонным тестом сгенерированным для нас, без реального тестирования любого кода. Затем мы напишем некоторые реальные тесты, которые вызывают некоторый написанный код и убедимся в его правильном поведении.
Давайте создадим новый проект библиотеки под названием adder
:
Содержимое файла src/lib.rs вашей библиотеки adder должно выглядеть как в листинге
11-1.
Файл: src/lib.rs
$
cargo new adder --lib
Created library `adder` project
$
cd adder

Листинг 11-1: Тестовый модуль и функция, сгенерированные автоматически с помощью
cargo new
Сейчас проигнорируем первые две строчки кода и сосредоточимся на функции, чтобы увидеть как она работает. Обратите внимание на синтаксис аннотации
#[test]
перед ключевым словом fn
. Этот атрибут сообщает компилятору, что это является заголовком тестирующей функции, так что функционал запускающий тесты на выполнение теперь знает, что это тестирующая функция. Также в составе модуля тестов tests могут быть вспомогательные функции, помогающие настроить и выполнить общие подготовительные операции, поэтому специальная аннотация важна для указания объявления функций тестами с использованием атрибута
#[test]
Тело функции использует макрос assert_eq!
, чтобы утверждать, что 2 + 2 равно 4. Это утверждение служит примером формата для типичного теста. Давайте запустим, чтобы увидеть, что этот тест проходит.
Команда cargo test выполнит все тесты в выбранном проекте и сообщит о результатах как в листинге 11-2:
Листинг 11-2: Вывод информации о работе автоматически сгенерированных тестов
Cargo скомпилировал и выполнил тест. После строк
Compiling
,
Finished и
Running мы видим строку running 1 test
. Следующая строка показывает имя созданной тест функции с названием it_works и результат её выполнения - ok
. Далее вы видите
#[cfg(test)]
mod tests {
#[test]
fn it_works
() { let result =
2
+
2
; assert_eq!
(result,
4
);
}
}
$
cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_works ... ok test result: ok. 1 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
обобщённую информацию о работе всех тестов. Текст test result: ok.
означает, что все тесты пройдены успешно и часть вывода
1 passed; 0 failed сообщает общее количество тестов, которые прошли или были ошибочными.
Поскольку у нас нет тестов, которые мы пометили как игнорируемые, в сводке отображается
0 ignored
. Мы также не отфильтровывали тесты для выполнения, поэтому конец сводки пишет
0 filtered out
. Мы поговорим про игнорирование и фильтрацию тестов в следующем разделе "Контролирование хода выполнения тестов"
Статистика
0 measured предназначена для тестов производительности. На момент написания этой статьи такие тесты доступны только в ночной сборке Rust. Посмотрите документацию о тестах производительности
, чтобы узнать больше.
Следующая часть вывода тестов начинается с
Doc-tests adder
- это информация о тестах в документации. У нас пока нет тестов документации, но Rust может компилировать любые примеры кода, которые находятся в API документации. Такая возможность помогает поддерживать документацию и код в синхронизированном состоянии. Мы поговорим о написании тестов документации в секции "Комментарии документации как тесты"
Главы 14. Пока просто проигнорируем часть
Doc-tests вывода.
Давайте поменяем название нашего теста и посмотрим что же измениться в строке вывода. Назовём нашу функцию it_works другим именем - exploration
:
Файл: src/lib.rs
Снова выполним команду cargo test
. Вывод показывает наименование нашей тест функции - exploration вместо it_works
:
#[cfg(test)]
mod tests {
#[test]
fn exploration
() { assert_eq!
(
2
+
2
,
4
);
}
}

Добавим ещё один тест, но в этот раз специально сделаем так, чтобы этот новый тест не отработал. Тест терпит неудачу, когда что-то паникует в тестируемой функции. Каждый тест запускается в новом потоке и когда главный поток видит, что тестовый поток упал,
то помечает тест как завершившийся аварийно. Мы говорили о простейшем способе вызвать панику в главе 9, используя для этого известный макрос panic!
. Введём код тест функции another
, как в файле src/lib.rs из листинга 11-3.
Файл: src/lib.rs
1   ...   22   23   24   25   26   27   28   29   ...   62
Листинг 11-3: Добавление второго теста, который завершится ошибкой, потому что мы вызываем panic!макросЗапустим команду cargo test. Вывод результатов показан в листинге 11-4, который сообщает, что тест exploration пройден, а another нет:$ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.59s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) 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 Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s #[cfg(test)]mod tests { #[test]fn exploration() { assert_eq!(2 + 2, 4); } #[test]fn another() { panic!("Make this test fail"); } }

Листинг 11-4. Результаты теста, когда один тест пройден, а другой нет
Вместо ok
, строка test tests::another сообщает
FAILED
. У нас есть два новых раздела между результатами и итогами. Первый раздел показывает детальную причину ошибки каждого теста. В данном случае тест another не сработал, потому что panicked at 'Make this test fail'
, произошло в строке 10 файла src/lib.rs. В следующем разделе перечисляют имена всех не пройденных тестов, что удобно, когда тестов очень много и есть много деталей про аварийное завершение. Мы можем использовать имя не пройденного теста для его дальнейшей отладки; мы больше поговорим о способах запуска тестов в разделе "Контролирование хода выполнения тестов"
Итоговая строка отображается в конце: общий результат нашего тестирования
FAILED
. У
нас один тест пройден и один тест завершён аварийно.
Теперь, когда вы увидели, как выглядят результаты теста при разных сценариях, давайте рассмотрим другие макросы полезные в тестах, кроме panic!
Проверка результатов с помощью макроса assert!
Макрос assert!
доступен из стандартной библиотеки и является удобным, когда вы хотите проверить что некоторое условие в тесте вычисляется в значение true
. Внутри макроса assert!
переданный аргумент вычисляется в логическое значение. Если оно true
, то assert!
в тесте ничего не делает и он считается пройденным. Если же значение вычисляется в false
, то макрос assert!
вызывает макрос panic!
, что делает тест
$
cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests test tests::another ... FAILED test tests::exploration ... ok 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 failures: tests::another 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'
аварийным. Использование макроса assert!
помогает проверить, что код функционирует как ожидалось.
В главе 5, листинга 5-15, мы использовали структуру
Rectangle и метод can_hold
,
который повторён в листинге 11-5. Давайте поместим этот код в файл src/lib.rs и напишем несколько тестов для него используя assert!
макрос.
Файл: src/lib.rs
Листинг 11-5: Использование структуры
Rectangle
и её метода
can_hold
из главы 5
Метод can_hold возвращает логическое значение, что означает, что она является идеальным вариантом использования в макросе assert!
. В листинге 11-6 мы пишем тест, который выполняет метод can_hold путём создания экземпляра
Rectangle шириной 8 и высотой 7 и убеждаемся, что он может содержать другой экземпляр
Rectangle имеющий ширину 5 и высоту 1.
Файл: src/lib.rs
Листинг 11-6: Теста для метода
can_hold
, который проверяет что больший прямоугольник действительно
может содержать меньший
#[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
}
}
#[cfg(test)]
mod tests { use super::*;
#[test]
fn larger_can_hold_smaller
() { let larger = Rectangle { width:
8
, height:
7
,
}; let smaller = Rectangle { width:
5
, height:
1
,
}; assert!
(larger.can_hold(&smaller));
}
}

Также, в модуле tests обратите внимание на новую добавленную строку use super::*;
Модуль tests является обычным и подчиняется тем же правилам видимости, которые мы обсуждали в главе 7
"Пути для ссылки на элементы внутри дерева модуля"
. Так как этот модуль tests является внутренним, нужно подключить тестируемый код из внешнего модуля в область видимости внутреннего модуля с тестами. Для этого используется глобальное подключение, так что все что определено во внешнем модуле становится доступным внутри tests модуля.
Мы назвали наш тест larger_can_hold_smaller и создали два нужных экземпляра
Rectangle
. Затем вызвали макрос assert!
и передали результат вызова larger.can_hold(&smaller)
в него. Это выражение должно возвращать true
, поэтому наш тест должен пройти. Давайте выясним!
Тест проходит. Теперь добавим другой тест, в этот раз мы попытаемся убедиться, что меньший прямоугольник не может содержать больший прямоугольник:
Файл: src/lib.rs
$
cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) 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
Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Источник: https://files.student-it.ru/previewfile/264058