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

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

На этот раз, когда мы выполним 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

1   ...   24   25   26   27   28   29   30   31   ...   62
Организация тестовКак упоминалось в начале главы, тестирование является сложной дисциплиной и разные люди используют разную терминологию и организацию. Сообщество 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
));
}
}
Источник: https://files.student-it.ru/previewfile/264058