Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 11-8: Проверка того, что условие вызовет макрос panic! Атрибут #[should_panic] следует после #[test] и до объявления текстовой функции. Посмотрим на вывод результата, когда тест проходит: pub struct Guess { value: i32 , } impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 || value > 100 { panic! ( "Guess value must be between 1 and 100, got {}." , value); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100 () { Guess::new( 200 ); } } Выглядит хорошо! Теперь давайте внесём ошибку в наш код, убрав условие о том, что функция new будет паниковать если значение больше 100: Когда мы запустим тест в листинге 11-8, он потерпит неудачу: $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/guessing_game- 57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests guessing_game running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s // --snip-- impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 { panic! ( "Guess value must be between 1 and 100, got {}." , value); } Guess { value } } } Мы получаем не очень полезное сообщение в этом случае, но когда мы смотрим на тестирующую функцию, мы видим, что она #[should_panic] . Аварийное выполнение, которое мы получили означает, что код в тестирующей функции не вызвал паники. Тесты, которые используют should_panic могут быть неточными, потому что они только указывают, что код вызвал панику. Тест с атрибутом should_panic пройдёт, даже если тест паникует по причине, отличной от той, которую мы ожидали. Чтобы сделать тесты с should_panic более точными, мы можем добавить необязательный параметр expected для атрибута should_panic . Такая детализация теста позволит удостовериться, что сообщение об ошибке содержит предоставленный текст. Например, рассмотрим модифицированный код для Guess в листинге 11-9, где new функция паникует с различными сообщениями в зависимости от того, является ли значение слишком маленьким или слишком большим. Файл: src/lib.rs $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/guessing_game- 57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ---- note: test did not panic as expected failures: tests::greater_than_100 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' Листинг 11-9: Проверка того, что условие вызовет макрос panic! с сообщением Этот тест пройдёт, потому что значение, которое мы поместили для should_panic в параметр атрибута expected является подстрокой сообщения, с которым функция Guess::new вызывает панику. Мы могли бы указать полное, ожидаемое сообщение для паники, в этом случае это будет Guess value must be less than or equal to 100, got 200 . То что вы выберите для указания как ожидаемого параметра у should_panic зависит от того, какая часть сообщения о панике уникальна или динамична, насколько вы хотите, чтобы ваш тест был точным. В этом случае достаточно подстроки из сообщения паники, чтобы гарантировать выполнение кода в тестовой функции else if value > 100 Чтобы увидеть, что происходит, когда тест should_panic неуспешно завершается с сообщением expected , давайте снова внесём ошибку в наш код, поменяв местами if value < 1 и else if value > 100 блоки: // --snip-- impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 { panic! ( "Guess value must be greater than or equal to 1, got {}." , value ); } else if value > 100 { panic! ( "Guess value must be less than or equal to 100, got {}." , value ); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "less than or equal to 100")] fn greater_than_100 () { Guess::new( 200 ); } } На этот раз, когда мы выполним should_panic тест, он потерпит неудачу: Сообщение об ошибке указывает, что этот тест действительно вызвал панику, как мы и ожидали, но сообщение о панике не включено ожидаемую строку 'Guess value must be less than or equal to 100' . Сообщение о панике, которое мы получили в этом случае, было Guess value must be greater than or equal to 1, got 200. Теперь мы можем начать выяснение, где ошибка! Использование Result Пока что мы написали тесты, которые паникуют, когда терпят неудачу. Мы также можем написать тесты которые используют Result ! Вот тест из листинга 11-1, переписанный с использованием Result и возвращающий Err вместо паники: if value < 1 { panic! ( "Guess value must be less than or equal to 100, got {}." , value ); } else if value > 100 { panic! ( "Guess value must be greater than or equal to 1, got {}." , value ); } $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/guessing_game- 57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ---- thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace note: panic did not contain expected string panic message: `"Guess value must be greater than or equal to 1, got 200."`, expected substring: `"less than or equal to 100"` failures: tests::greater_than_100 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' Функция it_works теперь имеет возвращаемый тип Result<(), String> . В теле функции, вместо вызова assert_eq! макроса, мы возвращаем Ok(()) когда тест успешно выполнен и Err со String внутри, когда тест не проходит. Написание тестов так, чтобы они возвращали Result позволяет использовать оператор "вопросительный знак" в теле тестов, который может быть удобным способом писать тесты, которые должны выполниться не успешно, если какая-либо операция внутри них возвращает вариант ошибки Err Вы не можете использовать аннотацию #[should_panic] в тестах, использующих Result . Чтобы утверждать, что операция возвращает вариант Err , не используйте оператор вопросительного знака для значения Result . Вместо этого используйте assert!(value.is_err()) Теперь, когда вы знаете несколько способов написания тестов, давайте взглянем на то, что происходит при запуске тестов и исследуем разные опции используемые с командой cargo test #[cfg(test)] mod tests { #[test] fn it_works () -> Result <(), String > { if 2 + 2 == 4 { Ok (()) } else { Err ( String ::from( "two plus two does not equal four" )) } } } Контролирование хода выполнения тестов Подобно тому, как cargo run компилирует ваш код и затем запускает полученный двоичный файл, cargo test компилирует ваш код в тестовом режиме и запускает полученный тестовый двоичный файл. Вы можете указать параметры командной строки, чтобы изменить поведение cargo test по умолчанию. Например, по умолчанию двоичный файл, созданный с помощью cargo test запускает все тесты параллельно и фиксирует выходные данные, созданные во время тестовых запусков, предотвращая отображение выходных данных и упрощая чтение выходных данных, связанных с результатами тестирования. Опции команды cargo test могут быть добавлены после, опции для тестов должны устанавливаться дополнительно (следовать далее). Для разделения этих двух типов аргументов используется разделитель -- . Чтобы узнать подробнее о доступных опциях команды cargo test - используйте опцию --help . Для того, чтобы узнать о доступных опциях, непосредственно для тестов, используйте команду cargo test -- --help Обратите внимание, что данную команду необходимо запускать внутри cargo-проекта (пакета). Выполнение тестов параллельно или последовательно Когда вы запускаете несколько тестов, по умолчанию они выполняются параллельно с использованием потоков. Это означает, что тесты завершатся быстрее, и вы сможете быстрее получить обратную связь о том, работает ли ваш код. Поскольку тесты выполняются одновременно, убедитесь, что ваши тесты не зависят друг от друга или от какого-либо общего состояния, включая общую среду, такую как текущий рабочий каталог или переменные среды. Например, когда тесты создают в одном и том же месте на диске файл с одним и тем же названием, читают из него данные, записывают их - вероятность ошибки в работе таких тестов (из-за конкурирования доступа к ресурсу, некорректных данных в файле) весьма высока. Решением будет использование уникальных имён создаваемых и используемых файлов каждым тестом в отдельности, либо выполнение таких тестов последовательно. Если вы не хотите запускать тесты параллельно или хотите более детальный контроль над количеством используемых потоков, можно установить флаг --test-threads и то количество потоков, которое вы хотите использовать для теста. Взгляните на следующий пример: Мы устанавливаем количество тестовых потоков равным 1 , указывая программе не использовать параллелизм. Выполнение тестов с использованием одного потока займёт $ cargo test -- -- test -threads=1 больше времени, чем их параллельное выполнение, но тесты не будут мешать друг другу, если они совместно используют состояние. Демонстрация результатов работы функции По умолчанию, если тест пройден, система управления запуска тестов блокирует вывод на печать, т.е. если вы вызовете макрос println! внутри кода теста и тест будет пройден, вы не увидите вывода на консоль результатов вызова println! . Если же тест не был пройден, все информационные сообщение, а также описание ошибки будет выведено на консоль. Например, в коде (11-10) функция выводит значение параметра с поясняющим текстовым сообщением, а также возвращает целочисленное константное значение 10 Далее следует тест, который имеет правильный входной параметр и тест, который имеет ошибочный входной параметр: Файл: src/lib.rs Listing 11-10: Тест функции, которая использует макрос println! Результат вывода на консоль команды cargo test : fn prints_and_returns_10 (a: i32 ) -> i32 { println! ( "I got the value {}" , a); 10 } #[cfg(test)] mod tests { use super::*; #[test] fn this_test_will_pass () { let value = prints_and_returns_10( 4 ); assert_eq! ( 10 , value); } #[test] fn this_test_will_fail () { let value = prints_and_returns_10( 8 ); assert_eq! ( 5 , value); } } Обратите внимание, что нигде в этом выводе мы не видим сообщения I got the value 4 , которое печатается при выполнении пройденного теста. Этот вывод был записан. Результат неудачного теста, I got the value 8 , появляется в разделе итоговых результатов теста, который также показывает причину неудачного теста. Для того, чтобы всегда видеть вывод на консоль корректно работающих программ, используйте флаг --show-output : Когда мы снова запускаем тесты из Листинга 11-10 с флагом --show-output , мы видим следующий результат: $ cargo test Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/silly_function- 160869f38cff9166) running 2 tests test tests::this_test_will_fail ... FAILED test tests::this_test_will_pass ... ok failures: ---- tests::this_test_will_fail stdout ---- I got the value 8 thread 'main' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail 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' $ cargo test -- --show-output Запуск подмножества тестов по имени Бывают случаи, когда в запуске всех тестов нет необходимости и нужно запустить только несколько тестов. Если вы работаете над функцией и хотите запустить тесты, которые исследуют её работу - это было бы удобно. Вы можете это сделать, используя команду cargo test , передав в качестве аргумента имена тестов. Для демонстрации, как запустить группу тестов, мы создадим группу тестов для функции add_two (код программы 11-11) и постараемся выбрать интересующие нас тесты при их запуске: Filename: src/lib.rs $ cargo test -- --show-output Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/silly_function- 160869f38cff9166) running 2 tests test tests::this_test_will_fail ... FAILED test tests::this_test_will_pass ... ok successes: ---- tests::this_test_will_pass stdout ---- I got the value 4 successes: tests::this_test_will_pass failures: ---- tests::this_test_will_fail stdout ---- I got the value 8 thread 'main' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail 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' Код программы 11-11: Три теста с различными именами Если вы выполните команду cargo test без уточняющих аргументов, все тесты выполнятся параллельно: Запуск одного теста Мы можем запустить один тест с помощью указания его имени в команде cargo test : pub fn add_two (a: i32 ) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn add_two_and_two () { assert_eq! ( 4 , add_two( 2 )); } #[test] fn add_three_and_two () { assert_eq! ( 5 , add_two( 3 )); } #[test] fn one_hundred () { assert_eq! ( 102 , add_two( 100 )); } } $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 3 tests test tests::add_three_and_two ... ok test tests::add_two_and_two ... ok test tests::one_hundred ... ok test result: ok. 3 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 Был запущен только тест с названием one_hundred ; имена остальных тестов отличались. Строка 2 filtered out в конце тестового вывода позволяет нам понять, что были ещё и другие тесты. Таким образом мы не можем указать имена нескольких тестов; будет использоваться только первое значение, указанное для cargo test . Но есть способ запустить несколько тестов. Использование фильтров для запуска нескольких тестов Мы можем указать часть имени теста, и будет запущен любой тест, имя которого соответствует этому значению. Например, поскольку имена двух наших тестов содержат add , мы можем запустить эти два, запустив cargo test add : Эта команда запускала все тесты с add в имени и отфильтровывала тест с именем one_hundred . Также обратите внимание, что модуль, в котором появляется тест, становится частью имени теста, поэтому мы можем запускать все тесты в модуле, фильтруя имя модуля. Игнорирование тестов Бывает, что некоторые тесты требуют продолжительного времени для своего исполнения, и вы хотите исключить их из исполнения при запуске cargo test . Вместо $ cargo test one_hundred Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.69s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::one_hundred ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s $ cargo test add Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests test tests::add_three_and_two ... ok test tests::add_two_and_two ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s перечисления в командной строке всех тестов, которые вы хотите запускать, вы можете аннотировать тесты, требующие много времени для прогона, атрибутом ignore , чтобы исключить их, как показано здесь: Файл: src/lib.rs После #[test] мы добавляем строку #[ignore] в тест, который хотим исключить. Теперь, когда мы запускаем наши тесты, it_works запускается, а expensive_test игнорируется: Функция expensive_test помечена как ignored . Если вы хотите выполнить только проигнорированные тесты, вы можете воспользоваться командой cargo test -- -- ignored : #[test] fn it_works () { assert_eq! ( 2 + 2 , 4 ); } #[test] #[ignore] fn expensive_test () { // code that takes an hour to run } $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests test expensive_test ... ignored test it_works ... ok test result: ok. 1 passed; 0 failed; 1 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 Управляя тем, какие тесты запускать, вы можете быть уверены, что результаты вашего cargo test будут быстрыми. Вы можете фильтровать тесты по имени при запуске. Вы также можете указать какие тесты должны быть проигнорированы при помощи ignored , а также отдельно запускать проигнорированные тесты при помощи cargo test -- -- ignored $ cargo test -- --ignored Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test expensive_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 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 |