Язык программирования Rust
Скачать 7.02 Mb.
|
Организация тестов Как упоминалось в начале главы, тестирование является сложной дисциплиной и разные люди используют разную терминологию и организацию. Сообщество Rust думает о тестах с точки зрения двух основных категорий: модульные тесты и интеграционные тесты. Модульные тесты это небольшие и более сфокусированные на тестировании одного модуля в отдельности или могут тестироваться приватные интерфейсы. Интеграционные тесты являются полностью внешними по отношению к вашей библиотеке и используют код библиотеки так же, как любой другой внешний код, используя только общедоступные интерфейсы и потенциально выполняя тестирование нескольких модулей в одном тесте. Написание обоих видов тестов важно для обеспечения того, чтобы кусочки вашей библиотеки по отдельности и вместе делали то, что вы ожидаете. Модульные тесты Целью модульных тестов является тестирование каждого блока кода, изолированное от остального функционала, чтобы можно было быстро понять, что работает некорректно или не так как ожидается. Мы разместим модульные тесты в папке src, в каждый тестируемый файл. Но в Rust принято создавать тестирующий модуль tests и код теста сохранять в файлы с таким же именем, как компоненты которые предстоит тестировать. Также необходимо добавить аннотацию cfg(test) к этому модулю. Модуль тестов и аннотация #[cfg(test)] Аннотация #[cfg(test)] у модуля с тестами указывает Rust компилировать и запускать только код тестов, когда выполняется команда cargo test , а не когда запускается cargo build . Это экономит время компиляции, если вы только хотите собрать библиотеку и сэкономить место для результирующих скомпилированных артефактов, потому что тесты не будут включены. Вы увидите что, по причине того, что интеграционные тесты помещаются в другой каталог им не нужна аннотация #[cfg(test)] . Тем не менее, так как модульные тесты идут в тех же файлах что и основной код, вы будете использовать # [cfg(test)] чтобы указать, что они не должны быть включены в скомпилированный результат. Напомним, что когда мы генерировали новый проект adder в первом разделе этой главы, то Cargo сгенерировал для нас код ниже: Файл: src/lib.rs Этот код является автоматически сгенерированным тестовым модулем. Атрибут cfg предназначен для конфигурации и говорит Rust, что следующий элемент должен быть включён только учитывая определённую опцию конфигурации. В этом случае опцией конфигурации является test , который предоставлен в Rust для компиляции и запуска текущих тестов. Используя атрибут cfg , Cargo компилирует только тестовый код при активном запуске тестов командой cargo test . Это включает в себя любые вспомогательные функции, которые могут быть в этом модуле в дополнение к функциям помеченным #[test] Тестирование приватных функций (private) Сообщество программистов не имеет однозначного мнения по поводу тестировать или нет приватные функции. В некоторых языках весьма сложно или даже невозможно тестировать такие функции. Независимо от того, какой технологии тестирования вы придерживаетесь, в Rust приватные функции можно тестировать. Рассмотрим листинг 11-12 с приватной функцией internal_adder Файл: src/lib.rs Листинг 11-12: Тестирование приватных функций Обратите внимание, что функция internal_adder не помечена как pub . Тесты — это просто Rust код, а модуль tests — это ещё один модуль. Как мы обсуждали в разделе #[cfg(test)] mod tests { #[test] fn it_works () { let result = 2 + 2 ; assert_eq! (result, 4 ); } } pub fn add_two (a: i32 ) -> i32 { internal_adder(a, 2 ) } fn internal_adder (a: i32 , b: i32 ) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn internal () { assert_eq! ( 4 , internal_adder( 2 , 2 )); } } “Пути для ссылки на элемент в дереве модулей“ , элементы в дочерних модулях могут использовать элементы из своих родительских модулей. В этом тесте мы помещаем все элементы родительского модуля test в область видимости с помощью use super::* и затем тест может вызывать internal_adder . Если вы считаете, что приватные функции не нужно тестировать, то Rust не заставит вас это сделать. Интеграционные тесты В Rust интеграционные тесты являются полностью внешними по отношению к вашей библиотеке. Они используют вашу библиотеку так же, как любой другой код, что означает, что они могут вызывать только функции, которые являются частью публичного API библиотеки. Их целью является проверка, много ли частей вашей библиотеки работают вместе правильно. У модулей кода правильно работающих самостоятельно, могут возникнуть проблемы при интеграции, поэтому тестовое покрытие интегрированного кода также важно. Для создания интеграционных тестов сначала нужен каталог tests . Каталог tests Мы создаём папку tests в корневой папке вашего проекта, рядом с папкой src. Cargo знает, что искать файлы с интеграционными тестами нужно в этой директории. После этого мы можем создать столько тестовых файлов, сколько захотим, и Cargo скомпилирует каждый из файлов в отдельный крейт. Давайте создадим интеграционный тест. Рядом с кодом из листинга 11-12, который всё ещё в файле src/lib.rs, создайте каталог tests, создайте новый файл с именем tests/integration_test.rs. Структура директорий должна выглядеть так: Введите код из листинга 11-13 в файл tests/integration_test.rs file: Файл: tests/integration_test.rs adder ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests └── integration_test.rs use adder; #[test] fn it_adds_two () { assert_eq! ( 4 , adder::add_two( 2 )); } Листинг 11-13: Интеграционная тест функция из крейта adder Каждый файл в каталоге tests представляет собой отдельный крейт, поэтому нам нужно подключить нашу библиотеку в область видимости каждого тестового крейта. По этой причине мы добавляем use adder в верхней части кода, что не нужно нам делать в модульных тестах. Нам не нужно комментировать код в tests/integration_test.rs с помощью #[cfg(test)] Cargo специальным образом обрабатывает каталог tests и компилирует файлы в этом каталоге только тогда, когда мы запускаем команду cargo test . Запустите cargo test сейчас: Выходные данные представлены тремя разделами: модульные тесты, интеграционные тесты и тесты документации. Обратите внимание, что если какой-нибудь тест в одной из секций не пройдёт, последующие секции выполняться не будут. Например, если модульный тест провалился, не будет выведено результатов интеграционных и документационных тестов, потому что эти тесты будут выполняться только в том случае, если все модульные тесты завершатся успешно. Первый раздел для модульных тестов такой же, как мы видели: одна строка для каждого модульного теста (один с именем internal , который мы добавили в листинге 11-12), а затем сводная строка для модульных тестов. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 1.31s Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6) running 1 test test tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test- 1082c4b063a8fbe6) running 1 test test it_adds_two ... 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 Раздел интеграционных тестов начинается со строки {код 0}Running tests/integration_test.rs{/код 0}. Далее идёт строка для каждой тестовой функции в этом интеграционном тесте и итоговая строка для результатов интеграционного теста непосредственно перед началом раздела Doc-tests adder Каждый файл интеграционного теста имеет свой собственный раздел, поэтому, если мы добавим больше файлов в каталог tests, то здесь будет больше разделов интеграционного теста. Мы всё ещё можем запустить определённую функцию в интеграционных тестах, указав имя тест функции в качестве аргумента в cargo test . Чтобы запустить все тесты в конкретном файле интеграционных тестов, используйте аргумент --test сопровождаемый именем файла у команды cargo test : Эта команда запускает только тесты в файле tests/integration_test.rs. Подмодули в интеграционных тестах По мере добавления большего количества интеграционных тестов, можно создать более одного файла в каталоге tests, чтобы легче организовывать их; например, вы можете сгруппировать функции тестирования по функциональности, которую они проверяют. Как упоминалось ранее, каждый файл в каталоге testsскомпилирован как отдельный крейт, что полезно для создания отдельных областей видимости, чтобы более точно имитировать то, как конечные пользователи будут использовать ваш крейт. Однако это означает, что файлы в каталоге tests ведут себя не так, как файлы в src, как вы узнали в Главе 7 относительно того как разделить код на модули и файлы. Различное поведение файлов в каталоге tests наиболее заметно, когда у вас есть набор вспомогательных функций, которые будут полезны в нескольких интеграционных тест файлах. Представим, что вы пытаетесь выполнить действия, описанные в разделе «Разделение модулей в разные файлы» главы 7, чтобы извлечь их в общий модуль. Например, вы создали файл tests/common.rs и поместили в него функцию setup , содержащую некоторый код, который вы будете вызывать из разных тестовых функций в нескольких тестовых файлах $ cargo test -- test integration_test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.64s Running tests/integration_test.rs (target/debug/deps/integration_test- 82e7799c1bc62298) running 1 test test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Файл: tests/common.rs Когда мы снова запустим тесты, мы увидим новый раздел в результатах тестов для файла common.rs, хотя этот файл не содержит никаких тестовых функций, более того, мы даже не вызывали функцию setup откуда либо: Упоминание файла common и появление в результатах выполнения тестов сообщения типа running 0 tests - это не то, чего мы хотели. Мы только хотели выделить некоторый общий код, который будет использоваться другими файлами интеграционных тестов. Чтобы модуль common больше не появлялся в результатах выполнения тестов, вместо файла tests/common.rs мы создадим файл tests/common/mod.rs. Директория проекта теперь выглядит следующим образом: pub fn setup () { // setup code specific to your library's tests would go here } $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.89s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/common.rs (target/debug/deps/common-92948b65e88960b4) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test- 92948b65e88960b4) running 1 test test it_adds_two ... 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 Здесь используется более раннее соглашение об именовании файлов, которое Rust также понимает. Мы говорили об этом в разделе “Альтернативные пути к файлам” главы 7. Именование файла таким образом говорит, что Rust не должен рассматривать модуль common как файл интеграционных тестов. Когда мы перемещаем код функции setup в файл tests/common/mod.rs и удаляем файл tests/common.rs, дополнительный раздел больше не будет отображаться в результатах тестов. Файлы в подкаталогах каталога tests не компилируются как отдельные крейты или не появляются в результатах выполнения тестов. После того, как мы создали файл tests/common/mod.rs, мы можем использовать его в любых файлах интеграционных тестов как обычный модуль. Вот пример вызова функции setup из теста it_adds_two в файле tests/integration_test.rs: Файл: tests/integration_test.rs Обратите внимание, что объявление mod common; совпадает с объявлением модуля, которое продемонстрировано в листинге 7-21. Затем в тестовой функции мы можем вызвать функцию common::setup() Интеграционные тесты для бинарных крейтов Если наш проект является бинарным крейтом, который содержит только src/main.rs и не содержит src/lib.rs, мы не сможем создать интеграционные тесты в папке tests и подключить функции определённые в файле src/main.rs в область видимости с помощью оператора use . Только библиотечные крейты могут предоставлять функции, которые можно использовать в других крейтах; бинарные крейты предназначены только для самостоятельного запуска. Это одна из причин, почему Rust проекты, которые генерируют исполняемые модули, обычно имеют простой файл src/main.rs,, который в свою очередь вызывает логику, ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests ├── common │ └── mod.rs └── integration_test.rs use adder; mod common; #[test] fn it_adds_two () { common::setup(); assert_eq! ( 4 , adder::add_two( 2 )); } которая находится в файле src/lib.rs. Используя такую структуру, интеграционные тесты могут проверить библиотечный крейт, используя оператор use для подключения важной функционала. Если этот важный функционал работает, то и небольшое количество кода в файле src/main.rs также будет работать, а значит этот небольшой объём кода не нуждается в проверке. Итоги Средства тестирования языка Rust предоставляют способ задать ожидаемое поведение кода, чтобы убедиться, что он всё ещё соответствует вашим ожиданиям даже после внесения изменений. Модульные тесты проверяют различные части библиотеки по отдельности и могут тестировать приватные детали реализации. Интеграционные тесты проверяют, что части библиотеки работают корректно сообща. Эти тесты используют для тестирования кода открытый API библиотеки, таким же образом, как его будет использовать внешний код. Хотя система типов Rust и правила владения помогают предотвратить некоторые виды ошибок, тесты по-прежнему важны для уменьшения количества логических ошибок, связанных с поведением вашего кода. Давайте объединим знания, полученные в этой и предыдущей главах, чтобы поработать над проектом! Проект с вводом/выводом (I/O): создание консольного приложения В этой главе вы примените многие знания, полученные ранее, а также познакомитесь с ещё неизученными API стандартной библиотеки. Мы создадим консольное приложение, который будет взаимодействовать с файлом и с консольным вводом / выводом, чтобы попрактиковаться в некоторых концепциях Rust, с которыми вы уже знакомы. Скорость, безопасность, компиляция в один исполняемый файл и кроссплатформенность делают Rust идеальным языком для создания консольных инструментов, так что в нашем проекте мы создадим свою собственную версию классической утилиты поиска grep , что расшифровывается, как "глобальное средство поиска и печати" (globally search a regular expression and print). В простейшем случае grep используется для поиска в выбранном файле указанного текста. Для этого утилита grep получает имя файла и текст в качестве аргументов. Далее она читает файл, находит и выводит строки, содержащие искомый текст. Попутно мы покажем, как сделать так, чтобы наше консольное приложение использовало возможности терминала, которые используются многими другими консольными инструментами. Мы будем читать значение переменной окружения, чтобы позволить пользователю настроить поведение нашего инструмента. Мы также будем печатать сообщения об ошибках в стандартный консольный поток ошибок ( stderr ) вместо стандартного вывода ( stdout ), чтобы, к примеру, пользователь мог перенаправить успешный вывод в файл, в то время, как сообщения об ошибках останутся на экране. Один из участников Rust-сообщества, Andrew Gallant, уже реализовал полнофункциональный, очень быстрый аналог программы grep и назвал его ripgrep По сравнению с ним, наша версия будет довольно простой, но эта глава даст вам знания, которые нужны для понимания реальных проектов, таких как ripgrep Наш проект grep будет использовать ранее изученные концепции: Организация кода (используя то, что вы узнали о модулях в главе 7 ) Использование векторов и строк (коллекции, глава 8 ) Обработка ошибок ( Глава 9 ) Использование типажей и времени жизни там, где это необходимо ( глава 10 ) Написание тестов ( Глава 11 ) Мы также кратко представим замыкания, итераторы и объекты типажи, которые будут объяснены подробно в главах 13 и 17 Принятие аргументов командной строки Создадим новый проект консольного приложения как обычно с помощью команды cargo new . Мы назовём проект minigrep , чтобы различать наше приложение от grep , которое возможно уже есть в вашей системе. Первая задача - заставить minigrep принимать два аргумента командной строки: путь к файлу и строку для поиска. То есть мы хотим иметь возможность запускать нашу программу через cargo run , с использованием двойного дефиса, чтобы указать, что следующие аргументы предназначены для нашей программы, а не для cargo , строки для поиска и пути к файлу в котором нужно искать, как описано ниже: В данный момент программа сгенерированная cargo new не может обрабатывать аргументы, которые мы ей передаём. Некоторые существующие библиотеки на crates.io могут помочь с написанием программы, которая принимает аргументы командной строки, но так как вы просто изучаете эту концепцию, давайте реализуем эту возможность сами. |