- Язык программирования Rust
- Организация тестов
- Модульные тесты
- Модуль тестов и аннотация #[cfg(test)]
- Тестирование приватных функций (private)
- Интеграционные тесты
- Каталог tests
- Подмодули в интеграционных тестах
- Интеграционные тесты для бинарных крейтов
- Итоги
- Язык программирования Rust
- Язык программирования Rust
- Контролирование хода выполнения тестов
- Выполнение тестов параллельно или последовательно
- Демонстрация результатов работы функции
- Запуск подмножества тестов по имени
- Запуск одного теста
- Использование фильтров для запуска нескольких тестов
- Игнорирование тестов
- The Rust Programming Language
- Rust By Example
- Unit testing
- Tests and ?
- Testing panics
- Running specific tests
- Ignoring tests
- The rustc book
- Tests
- Test attributes
- CLI arguments
- Filters
- Action options
- --list
- -h, --help
- Selection options
- --test
- --bench
- --exact
- --skip FILTER
- --ignored
- --include-ignored
- --exclude-should-panic
- Execution options
- --test-threads NUM_THREADS
- --force-run-in-process
- --ensure-time
- --shuffle
- --shuffle-seed SEED
- Output options
- -q, --quiet
- --nocapture
- --show-output
- --color COLOR
- --format FORMAT
- --logfile PATH
- --report-time
- Unstable options
- Benchmarks
- Custom test frameworks
Язык программирования Rust
Организация тестов
Как упоминалось в начале главы, тестирование является сложной дисциплиной и разные люди используют разную терминологию и организацию. Сообщество Rust думает о тестах с точки зрения двух основных категорий: модульные тесты и интеграционные тесты. Модульные тесты это небольшие и более сфокусированные на тестировании одного модуля в отдельности или могут тестироваться приватные интерфейсы. Интеграционные тесты являются полностью внешними по отношению к вашей библиотеке и используют код библиотеки так же, как любой другой внешний код, используя только общедоступные интерфейсы и потенциально выполняя тестирование нескольких модулей в одном тесте.
Написание обоих видов тестов важно для обеспечения того, чтобы кусочки вашей библиотеки по отдельности и вместе делали то, что вы ожидаете.
Модульные тесты
Целью модульных тестов является тестирование каждого блока кода, изолированное от остального функционала, чтобы можно было быстро понять, что работает некорректно или не так как ожидается. Мы разместим модульные тесты в папке src, в каждый тестируемый файл. Но в Rust принято создавать тестирующий модуль tests
и код теста сохранять в файлы с таким же именем, как компоненты которые предстоит тестировать. Также необходимо добавить аннотацию cfg(test)
к этому модулю.
Модуль тестов и аннотация #[cfg(test)]
Напомним, что когда мы генерировали новый проект adder
в первом разделе этой главы, то Cargo сгенерировал для нас код ниже:
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
Тестирование приватных функций (private)
Сообщество программистов не имеет однозначного мнения по поводу тестировать или нет приватные функции. В некоторых языках весьма сложно или даже невозможно тестировать такие функции. Независимо от того, какой технологии тестирования вы придерживаетесь, в Rust приватные функции можно тестировать. Рассмотрим листинг 11-12 с приватной функцией internal_adder
.
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));
}
}
Листинг 11-12: Тестирование приватных функций
Обратите внимание, что функция internal_adder
не помечена как pub
. Тесты — это просто Rust код, а модуль tests
— это ещё один модуль. Как мы обсуждали в разделе “Пути для ссылки на элемент в дереве модулей“, элементы в дочерних модулях могут использовать элементы из своих родительских модулей. В этом тесте мы помещаем все элементы родительского модуля test
в область видимости с помощью use super::*
и затем тест может вызывать internal_adder
. Если вы считаете, что приватные функции не нужно тестировать, то Rust не заставит вас это сделать.
Интеграционные тесты
В Rust интеграционные тесты являются полностью внешними по отношению к вашей библиотеке. Они используют вашу библиотеку так же, как любой другой код, что означает, что они могут вызывать только функции, которые являются частью публичного API библиотеки. Их целью является проверка, много ли частей вашей библиотеки работают вместе правильно. У модулей кода правильно работающих самостоятельно, могут возникнуть проблемы при интеграции, поэтому тестовое покрытие интегрированного кода также важно. Для создания интеграционных тестов сначала нужен каталог tests .
Каталог tests
Мы создаём папку tests в корневой папке вашего проекта, рядом с папкой src. Cargo знает, что искать файлы с интеграционными тестами нужно в этой директории. После этого мы можем создать столько тестовых файлов, сколько захотим, и Cargo скомпилирует каждый из файлов в отдельный крейт.
Давайте создадим интеграционный тест. Рядом с кодом из листинга 11-12, который всё ещё в файле src/lib.rs, создайте каталог tests, создайте новый файл с именем tests/integration_test.rs. Структура директорий должна выглядеть так:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
Введите код из листинга 11-13 в файл tests/integration_test.rs file:
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
Листинг 11-13: Интеграционная тест функция из крейта adder
Каждый файл в каталоге tests
представляет собой отдельный крейт, поэтому нам нужно подключить нашу библиотеку в область видимости каждого тестового крейта. По этой причине мы добавляем use adder
в верхней части кода, что не нужно нам делать в модульных тестах.
$ 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
Выходные данные представлены тремя разделами: модульные тесты, интеграционные тесты и тесты документации. Обратите внимание, что если какой-нибудь тест в одной из секций не пройдёт, последующие секции выполняться не будут. Например, если модульный тест провалился, не будет выведено результатов интеграционных и документационных тестов, потому что эти тесты будут выполняться только в том случае, если все модульные тесты завершатся успешно.
Первый раздел для модульных тестов такой же, как мы видели: одна строка для каждого модульного теста (один с именем internal
, который мы добавили в листинге 11-12), а затем сводная строка для модульных тестов.
Раздел интеграционных тестов начинается со строки Running tests/integration_test.rs
. Далее идёт строка для каждой тестовой функции в этом интеграционном тесте и итоговая строка для результатов интеграционного теста непосредственно перед началом раздела Doc-tests adder
.
Каждый файл интеграционного теста имеет свой собственный раздел, поэтому, если мы добавим больше файлов в каталог tests, то здесь будет больше разделов интеграционного теста.
Мы всё ещё можем запустить определённую функцию в интеграционных тестах, указав имя тест функции в качестве аргумента в cargo test
. Чтобы запустить все тесты в конкретном файле интеграционных тестов, используйте аргумент --test
сопровождаемый именем файла у команды cargo test
:
$ 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/integration_test.rs.
Подмодули в интеграционных тестах
По мере добавления большего количества интеграционных тестов, можно создать более одного файла в каталоге tests, чтобы легче организовывать их; например, вы можете сгруппировать функции тестирования по функциональности, которую они проверяют. Как упоминалось ранее, каждый файл в каталоге tests скомпилирован как отдельный крейт, что полезно для создания отдельных областей видимости, чтобы более точно имитировать то, как конечные пользователи будут использовать ваш крейт. Однако это означает, что файлы в каталоге tests ведут себя не так, как файлы в src, как вы узнали в Главе 7 относительно того как разделить код на модули и файлы.
Различное поведение файлов в каталоге tests наиболее заметно, когда у вас есть набор вспомогательных функций, которые будут полезны в нескольких интеграционных тестовых файлах. Представим, что вы пытаетесь выполнить действия, описанные в разделе «Разделение модулей в разные файлы» главы 7, чтобы извлечь их в общий модуль. Например, вы создали файл tests/common.rs и поместили в него функцию setup
, содержащую некоторый код, который вы будете вызывать из разных тестовых функций в нескольких тестовых файлах
pub fn setup() {
// setup code specific to your library's tests would go here
}
Когда мы снова запустим тесты, мы увидим новый раздел в результатах тестов для файла common.rs, хотя этот файл не содержит никаких тестовых функций, более того, мы даже не вызывали функцию setup
откуда либо:
$ 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
Упоминание файла common
и появление в результатах выполнения тестов сообщения типа running 0 tests
– это не то, чего мы хотели. Мы только хотели выделить некоторый общий код, который будет использоваться другими файлами интеграционных тестов.
Чтобы модуль common
больше не появлялся в результатах выполнения тестов, вместо файла tests/common.rs мы создадим файл tests/common/mod.rs. Директория проекта теперь выглядит следующим образом:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
Здесь используется более раннее соглашение об именовании файлов, которое 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:
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
Обратите внимание, что объявление mod common;
совпадает с объявлением модуля, которое продемонстрировано в листинге 7-21. Затем в тестовой функции мы можем вызвать функцию common::setup()
.
Интеграционные тесты для бинарных крейтов
Если наш проект является бинарным крейтом, который содержит только src/main.rs и не содержит src/lib.rs, мы не сможем создать интеграционные тесты в папке tests и подключить функции определённые в файле src/main.rs в область видимости с помощью оператора use
. Только библиотечные крейты могут предоставлять функции, которые можно использовать в других крейтах; бинарные крейты предназначены только для самостоятельного запуска.
Это одна из причин, почему проекты на Rust, которые генерируют исполняемые модули, обычно имеют простой файл src/main.rs, который в свою очередь вызывает логику, которая находится в файле src/lib.rs. Используя такую структуру, интеграционные тесты могут проверить библиотечный крейт, используя оператор use
для подключения важной функционала. Если этот важный функционал работает, то и небольшое количество кода в файле src/main.rs также будет работать, а значит этот небольшой объём кода не нуждается в проверке.
Итоги
Средства тестирования языка Rust предоставляют способ задать ожидаемое поведение кода, чтобы убедиться, что он всё ещё соответствует вашим ожиданиям даже после внесения изменений. Модульные тесты проверяют различные части библиотеки по отдельности и могут тестировать приватные детали реализации. Интеграционные тесты проверяют, что части библиотеки работают корректно сообща. Эти тесты используют для тестирования кода открытый API библиотеки, таким же образом, как его будет использовать внешний код. Хотя система типов Rust и правила владения помогают предотвратить некоторые виды ошибок, тесты по-прежнему важны для уменьшения количества логических ошибок, связанных с поведением вашего кода.
Давайте объединим знания, полученные в этой и предыдущей главах, чтобы поработать над проектом!
Язык программирования Rust
Язык программирования Rust
Контролирование хода выполнения тестов
Подобно тому, как cargo run
выполняет компиляцию вашего кода, а затем запускает полученный двоичный файл, cargo test
компилирует ваш код в режиме тестирования и запускает полученный бинарник с тестами. Двоичный файл, создаваемый cargo test
, по умолчанию запускает все тесты параллельно и перехватывает вывод, генерируемый во время выполнения тестов, предотвращая их вывод на экран для облегчения чтения вывода, относящегося к результатам тестирования. Однако вы можете указать параметры командной строки, чтобы изменить это поведение по умолчанию.
Часть параметров командной строки передаётся в cargo test
, а часть – в итоговый двоичный файл с тестами. Чтобы разделить эти два типа аргументов, нужно сначала указать аргументы, которые идут в cargo test
, затем использовать разделитель --
, а потом те, которые попадут в двоичный файл теста. Выполнение cargo test --help
выводит опции, которые вы можете использовать с cargo test
, а выполнение cargo test -- --help
выводит опции, которые вы можете использовать за разделителем.
Выполнение тестов параллельно или последовательно
Когда вы запускаете несколько тестов, по умолчанию они выполняются параллельно с использованием потоков, что означает, что они завершатся быстрее, и вы быстрее получите результаты. Поскольку тесты выполняются параллельно, вы должны убедиться, что ваши тесты не зависят друг от друга или от какого-либо общего состояния, включая общее окружение, например, текущий рабочий каталог или переменные окружения.
Например, допустим, каждый из ваших тестов запускает код, который создаёт файл на диске с именем test-output.txt и записывает некоторые данные в этот файл. Затем каждый тест считывает данные из этого файла и утверждает, что файл содержит определённое значение, которое в каждом тесте разное. Поскольку все тесты выполняются одновременно, один из тестов может перезаписать файл в промежутке между записью и чтением файла другим тестом. Тогда второй тест потерпит неудачу, но не потому, что код неверен, а потому, что эти тесты мешали друг другу при параллельном выполнении. Одно из решений – убедиться, что каждый тест пишет в свой отдельный файл; другое решение – запускать тесты по одному.
Если вы не хотите запускать тесты параллельно или хотите более детальный контроль над количеством используемых потоков, можно установить флаг --test-threads
и то количество потоков, которое вы хотите использовать для теста. Взгляните на следующий пример:
$ cargo test -- --test-threads=1
Мы устанавливаем количество тестовых потоков равным 1
, указывая программе не использовать параллелизм. Выполнение тестов с использованием одного потока займёт больше времени, чем их параллельное выполнение, но тесты не будут мешать друг другу, если они совместно используют состояние.
Демонстрация результатов работы функции
По умолчанию, если тест пройден, система управления запуска тестов блокирует вывод на печать, т.е. если вы вызовете макрос println!
внутри кода теста и тест будет пройден, вы не увидите вывода на консоль результатов вызова println!
. Если же тест не был пройден, все информационные сообщение, а также описание ошибки будет выведено на консоль.
Например, в коде (11-10) функция выводит значение параметра с поясняющим текстовым сообщением, а также возвращает целочисленное константное значение 10
. Далее следует тест, который имеет правильный входной параметр и тест, который имеет ошибочный входной параметр:
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);
}
}
Листинг 11-10: Тест функции, которая использует макрос println!
Результат вывода на консоль команды cargo test
:
$ 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`
Обратите внимание, что нигде в этом выводе мы не видим сообщения I got the value 4
, которое печатается при выполнении пройденного теста. Этот вывод был записан. Результат неудачного теста, I got the value 8
, появляется в разделе итоговых результатов теста, который также показывает причину неудачного теста.
Если мы хотим видеть напечатанные результаты прохождения тестов, мы можем сказать Rust, чтобы он также показывал результаты успешных тестов с помощью --show-output
.
$ cargo test -- --show-output
Когда мы снова запускаем тесты из Листинга 11-10 с флагом --show-output
, мы видим следующий результат:
$ 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`
Запуск подмножества тестов по имени
Бывают случаи, когда в запуске всех тестов нет необходимости и нужно запустить только несколько тестов. Если вы работаете над функцией и хотите запустить тесты, которые исследуют её работу – это было бы удобно. Вы можете это сделать, используя команду cargo test
, передав в качестве аргумента имена тестов.
Для демонстрации, как запустить группу тестов, мы создадим группу тестов для функции add_two
function, как показано в Листинге 11-11, и постараемся выбрать какие из них запускать.
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));
}
}
Листинг 11-11: Три теста с различными именами
Если вы выполните команду cargo test
без уточняющих аргументов, все тесты выполнятся параллельно:
$ 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
Запуск одного теста
Мы можем запустить один тест с помощью указания его имени в команде 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
Был запущен только тест с названием one_hundred
; два других теста не соответствовали этому названию. Результаты теста с помощью вывода 2 filtered out
дают нам понять, что у нас было больше тестов, но они не были запущены.
Таким образом мы не можем указать имена нескольких тестов; будет использоваться только первое значение, указанное для cargo test
. Но есть способ запустить несколько тестов.
Использование фильтров для запуска нескольких тестов
Мы можем указать часть имени теста, и будет запущен любой тест, имя которого соответствует этому значению. Например, поскольку имена двух наших тестов содержат add
, мы можем запустить эти два, запустив cargo test add
:
$ 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
Эта команда запускала все тесты с add
в имени и отфильтровывала тест с именем one_hundred
. Также обратите внимание, что модуль, в котором появляется тест, становится частью имени теста, поэтому мы можем запускать все тесты в модуле, фильтруя имя модуля.
Игнорирование тестов
Бывает, что некоторые тесты требуют продолжительного времени для своего исполнения, и вы хотите исключить их из исполнения при запуске cargo test
. Вместо перечисления в командной строке всех тестов, которые вы хотите запускать, вы можете аннотировать тесты, требующие много времени для прогона, атрибутом ignore
, чтобы исключить их, как показано здесь:
#[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
Функция expensive_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
Управляя тем, какие тесты запускать, вы можете быть уверены, что результаты вашего cargo test
будут быстрыми. Когда вы дойдёте до момента, где имеет смысл проверить результаты тестов ignored
, и у вас есть время дождаться их результатов, вы можете запустить их с помощью cargo test -- --ignored
. Если вы хотите запустить все тесты независимо от того, игнорируются они или нет, выполните cargo test -- --include-ignored
.
The Rust Programming Language
Rust By Example
Unit testing
Tests are Rust functions that verify that the non-test code is functioning in
the expected manner. The bodies of test functions typically perform some setup,
run the code we want to test, then assert whether the results are what we
expect.
Tests fail when something in the test function panics. There are some
helper macros:
assert!(expression)
– panics if expression evaluates tofalse
.assert_eq!(left, right)
andassert_ne!(left, right)
– testing left and
right expressions for equality and inequality respectively.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// This is a really bad adding function, its purpose is to fail in this
// example.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)]
mod tests {
// Note this useful idiom: importing names from outer (for mod tests) scope.
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_bad_add() {
// This assert would fire and test will fail.
// Please note, that private functions can be tested too!
assert_eq!(bad_add(1, 2), 3);
}
}
Tests can be run with cargo test
.
$ cargo test
running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok
failures:
---- tests::test_bad_add stdout ----
thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
left: `-1`,
right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::test_bad_add
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
Tests and ?
None of the previous unit test examples had a return type. But in Rust 2018,
your unit tests can return Result<()>
, which lets you use ?
in them! This
can make them much more concise.
fn sqrt(number: f64) -> Result<f64, String> { if number >= 0.0 { Ok(number.powf(0.5)) } else { Err("negative floats don't have square roots".to_owned()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_sqrt() -> Result<(), String> { let x = 4.0; assert_eq!(sqrt(x)?.powf(2.0), x); Ok(()) } }
Testing panics
pub fn divide_non_zero_result(a: u32, b: u32) -> u32 { if b == 0 { panic!("Divide-by-zero error"); } else if a < b { panic!("Divide result is zero"); } a / b } #[cfg(test)] mod tests { use super::*; #[test] fn test_divide() { assert_eq!(divide_non_zero_result(10, 2), 5); } #[test] #[should_panic] fn test_any_panic() { divide_non_zero_result(1, 0); } #[test] #[should_panic(expected = "Divide result is zero")] fn test_specific_panic() { divide_non_zero_result(1, 10); } }
Running these tests gives us:
$ cargo test running 3 tests test tests::test_any_panic ... ok test tests::test_divide ... ok test tests::test_specific_panic ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests tmp-test-should-panic running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running specific tests
To run specific tests one may specify the test name to
cargo test
command.$ cargo test test_any_panic running 1 test test tests::test_any_panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out Doc-tests tmp-test-should-panic running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
To run multiple tests one may specify part of a test name that matches all the
tests that should be run.$ cargo test panic running 2 tests test tests::test_any_panic ... ok test tests::test_specific_panic ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out Doc-tests tmp-test-should-panic running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Ignoring tests
pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 2), 4); } #[test] fn test_add_hundred() { assert_eq!(add(100, 2), 102); assert_eq!(add(2, 100), 102); } #[test] #[ignore] fn ignored_test() { assert_eq!(add(0, 0), 0); } }
$ cargo test running 3 tests test tests::ignored_test ... ignored test tests::test_add ... ok test tests::test_add_hundred ... ok test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out Doc-tests tmp-ignore running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out $ cargo test -- --ignored running 1 test test tests::ignored_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests tmp-ignore running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
The rustc book
Tests
rustc
has a built-in facility for building and running tests for a crate.
More information about writing and running tests may be found in the Testing
Chapter of the Rust Programming Language book.#[test] fn it_works() { assert_eq!(2 + 2, 4); }
Tests “pass” if they return without an error. They “fail” if they panic, or
return a type such asResult
that implements theTermination
trait
with a non-zero value.
- The crate will be built as a
bin
crate type, forcing it to be an
executable. - Links the executable with
libtest
, the test harness that is part of the
standard library, which handles running the tests. - Synthesizes a
main
function which will process command-line arguments
and run the tests. This newmain
function will replace any existingmain
function as the entry point of the executable, though the existingmain
will still be compiled. - Enables the
test
cfg option, which allows your code to use conditional
compilation to detect if it is being built as a test. - Enables building of functions annotated with the
test
andbench
attributes, which will be run by the test
harness.
After the executable is created, you can run it to execute the tests and
receive a report on what passes and fails. If you are using Cargo to manage
your project, it has a built-in cargo test
command which handles all of
this automatically. An example of the output looks like this:
running 4 tests
test it_works ... ok
test check_valid_args ... ok
test invalid_characters ... ok
test walks_the_dog ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Note: Tests must be built with the
unwind
panic
strategy. This is because all tests run in the same
process, and they are intended to catch panics, which is not possible with
theabort
strategy. See the unstable-Z panic-abort-tests
option for
experimental support of theabort
strategy by spawning tests in separate
processes.
Test attributes
#[test]
— Indicates a function is a test to be run.#[bench]
— Indicates a function is a benchmark to be
run. Benchmarks are currently unstable and only available in the nightly
channel, see the unstable docs for more details.#[should_panic]
— Indicates that the test
function will only pass if the function panics.#[ignore]
— Indicates that the test function will be
compiled, but not run by default. See the--ignored
and
--include-ignored
options to run these tests.
CLI arguments
The libtest harness has several command-line arguments to control its
behavior.
Note: When running with
cargo test
, the libtest CLI arguments must be
passed after the--
argument to differentiate between flags for Cargo and
those for the harness. For example:cargo test -- --nocapture
Filters
Positional arguments (those without a -
prefix) are treated as filters which
will only run tests whose name matches one of those strings. The filter will
match any substring found in the full path of the test function. For example,
if the test function it_works
is located in the module
utils::paths::tests
, then any of the filters works
, path
, utils::
, or
utils::paths::tests::it_works
will match that test.
Action options
--list
Prints a list of all tests and benchmarks. Does not run any of the tests.
Filters can be used to list only matching tests.
-h
, --help
Displays usage information and command-line options.
Selection options
--test
This is the default mode where all tests will be run as well as running all
benchmarks with only a single iteration (to ensure the benchmark works,
without taking the time to actually perform benchmarking). This can be
combined with the --bench
flag to run both tests and perform full
benchmarking.
--bench
This runs in a mode where tests are ignored, and only runs benchmarks. This
can be combined with --test
to run both benchmarks and tests.
--exact
This forces filters to match the full path of the test exactly.
For example, if the test it_works
is in the module utils::paths::tests
,
then only the string utils::paths::tests::it_works
will match that test.
--skip
FILTER
Skips any tests whose name contains the given FILTER string. This flag may
be passed multiple times.
--ignored
Runs only tests that are marked with the ignore
attribute.
--include-ignored
Runs both ignored and non-ignored tests.
--exclude-should-panic
Excludes tests marked with the should_panic
attribute.
Execution options
--test-threads
NUM_THREADS
Sets the number of threads to use for running tests in parallel. By default,
uses the amount of concurrency available on the hardware as indicated by
available_parallelism
.
This can also be specified with the RUST_TEST_THREADS
environment variable.
--force-run-in-process
Forces the tests to run in a single process when using the abort
panic
strategy.
--ensure-time
--shuffle
Runs the tests in random order, as opposed to the default alphabetical order.
This may also be specified by setting the RUST_TEST_SHUFFLE
environment
variable to anything but 0
.
The random number generator seed that is output can be passed to
--shuffle-seed
to run the tests in the same order
again.
Note that --shuffle
does not affect whether the tests are run in parallel. To
run the tests in random order sequentially, use --shuffle --test-threads 1
.
--shuffle-seed
SEED
Like --shuffle
, but seeds the random number generator with
SEED. Thus, calling the test harness with --shuffle-seed
SEED twice runs
the tests in the same order both times.
SEED is any 64-bit unsigned integer, for example, one produced by
--shuffle
.
This can also be specified with the RUST_TEST_SHUFFLE_SEED
environment
variable.
Output options
-q
, --quiet
Displays one character per test instead of one line per test. This is an alias
for --format=terse
.
--nocapture
Does not capture the stdout and stderr of the test, and allows tests to print
to the console. Usually the output is captured, and only displayed if the test
fails.
This may also be specified by setting the RUST_TEST_NOCAPTURE
environment
variable to anything but 0
.
--show-output
Displays the stdout and stderr of successful tests after all tests have run.
Contrast this with --nocapture
which allows tests to print
while they are running, which can cause interleaved output if there are
multiple tests running in parallel, --show-output
ensures the output is
contiguous, but requires waiting for all tests to finish.
--color
COLOR
Control when colored terminal output is used. Valid options:
auto
: Colorize if stdout is a tty and--nocapture
is not
used. This is the default.always
: Always colorize the output.never
: Never colorize the output.
--format
FORMAT
Controls the format of the output. Valid options:
pretty
: This is the default format, with one line per test.terse
: Displays only a single character per test.--quiet
is an alias for this option.json
: Emits JSON objects, one per line. ⚠️ 🚧 This option is
unstable, and requires the-Z unstable-options
flag.
See tracking issue #49359
for more information.
--logfile
PATH
Writes the results of the tests to the given file.
--report-time
Unstable options
Some CLI options are added in an “unstable” state, where they are intended for
experimentation and testing to determine if the option works correctly, has
the right design, and is useful. The option may not work correctly, break, or
change at any time. To signal that you acknowledge that you are using an
unstable option, they require passing the -Z unstable-options
command-line
flag.
Benchmarks
Custom test frameworks
35 лет назад пилот-любитель Матиас Руст перелетел из Хельсинки в Москву, преодолев на пути все советские системы ПВО. Видевшие его советские истребители не получали команд сбивать или сажать самолет, а несколько раз его принимали за свой. Руст приземлился на Большом Москворецком мосту, рядом с Красной площадью возле Кремля, и спокойно раздавал там автографы, пока за ним не пришли из КГБ. Подробности о мотивах, причинах успеха летчика и его дальнейшей судьбе — в материале «Газеты.Ru».
28 мая 1987 года молодой западногерманский пилот-любитель Матиас Руст навсегда вошел в историю, сумев беспрепятственно пролететь от Хельсинки до Москвы сквозь глубокоэшелонированную систему ПВО советского государства, считавшуюся тогда самой надежной в мире. Он посадил свой легкомоторный самолет Cessna 172 Skyhawk на Большом Москворецком мосту, докатившись затем до Васильевского спуска к Храму Василия Блаженного. Его «подвиг» немедленно послужил источником мемов — вроде переименования Красной площади в «Шереметьево-3», о нем слагали песни перестроечные барды, им невольно восхищались даже те, кто называл его не иначе как «злостным и безответственным авиахулиганом». Он, возможно, ускорил перестройку, дав удачный повод генсеку Михаилу Горбачеву показательно уволить сотни высокопоставленных несговорчивых военных во главе с министром обороны Сергеем Соколовым, проведя тем самым самую масштабную чистку со времен сталинских репрессий 1930-х годов, но при этом пилот никогда не был принят самим Горбачевым, на встречу с которым он искренне надеялся.
«Мне необходимо было встретиться с Михаилом Горбачевым», — заявит он затем на суде в ответ на вопрос: «Зачем вы прилетели в Москву?»
Невероятное стечение обстоятельств, связанных с полетом Руста, вполне предсказуемым образом стало подпитывать разного рода теории заговора, в которых фигурировали и НАТО, и ЦРУ, и сам Горбачев, отдавший якобы соответствующие тайные указания председателю КГБ СССР Владимиру Крючкову, и даже чья-то подводная лодка, создавшая странное масляное пятно на месте мнимой аварии самолета Руста в Финском заливе. Однако время не оставило от этих предположений практически ничего, в полном соответствии с известным тезисом: «Никогда не приписывайте злому умыслу то, что вполне можно объяснить глупостью».
Спустя 35 лет этого бывшего отчаянного подростка — на момент посадки Матиасу Русту не исполнилось даже 19 лет — никто особенно не чествует, столкновения с законом лишили его былого морального авторитета; книга воспоминаний, написанная им в 2012 году, провалилась в продаже, новых откровений от него не ждут. Когда-то западная пресса восхищенно называла его «новым Красным Бароном» и «Дон Кихотом небес». В Эстонии на месте пересечения Рустом границы советского государства на днях даже открыли памятник в виде посадочной полосы, но это всего лишь частная инициатива.
В России Руста тоже вспоминали без симпатии. «Руст пробуравил спортивной «Сессной» воздушную границу СССР; хваленое ПВО ядерной сверхдержавы обнаружило полную беспомощность системы в целом; через брешь, пробитую Рустом, вышел воздух, и шарик начал неумолимо сдуваться», — писал пятнадцать лет назад Александр Архангельский.
Итальянская газета La Repubblica тогда же взяла интервью у отчаянного пилота, где он признавался: «С помощью маленькой «Ceссны» я решил осуществить мечту: прилететь с Запада прямо на Красную площадь. Как жест мира: полет в качестве символического моста между двумя мирами». Так или иначе, но встряску испытала не только советская система, но и западный мир. Для населяющих его обывателей Советский Союз из мощной «Империи Зла» (Evil Empire), как его называл Рональд Рейган после сбитого в 1984 году южнокорейского «Боинга» с 269 жертвами, внезапно превратился во вполне доброжелательного соседа, не решающегося сбить даже явного нарушителя, направлявшегося к сердцу столицы, — то ли из-за всеобщего развала, то ли просто из-за нерешительности военных, по рукам и ногам связанных инструкцией до последнего не применять оружие.
В другом интервью, взятом у Руста американскими научными журналистами в июле 2005-го, пилот также говорит о своей идее «построить воображаемый мост», прилетев в Москву, и о 20-страничном манифесте, который он планировал передать Горбачеву и в котором говорилось о том, как добиться мира во всем мире. «И как бы Рейган продолжал говорить, что это «Империя Зла», если я на маленьком самолете могу прилететь туда и остаться целым и невредимым?» — спрашивал Руст.
В ходе своего полета он видел советский МиГ-23 и пилота в нем, старшего лейтенанта Пучнина, готового немедленно применить оружие или, как сам старлей потом признавался, сжечь самолетик огнем из сопла, включив форсаж, — но МиГ был отозван, тревогу в очередной раз не объявили, а «Ceссну» в очередной раз потеряли.
Оказавшись над Москвой, Руст долго не мог найти Красную площадь, поскольку привык, что центры западных городов отмечают ряды сверкающих офисных башен. Но наконец он увидел характерную стену с башенками, окружающую Кремль. После этого он начал кружить, выбирая место для посадки.
«Сначала я думал, что, может быть, мне следует приземлиться внутри кремлевской стены, но потом понял, что, хотя места там предостаточно, КГБ может со мной сделать там нечто ужасное, — вспоминает он. — И если бы я приземлился за стеной, меня увидели бы от силы лишь несколько человек, они могли бы просто забрать меня и все потом отрицать. Приземлившись на открытой площади, я показался множеству людей, и в КГБ не могли арестовать меня незаметно и лгать потом об этом. Так что ради собственной безопасности я и отказался от этой идеи».
Кружа над Кремлем, Руст заметил подходящий шестиполосный мост через Москву-реку, ведущий на Красную площадь. Единственными препятствием были провода, натянутые с обоих концов моста и по его середине. Раст прикинул, что места там достаточно, чтобы пройти над первой линией проводов и затем, приземлившись, подрулить под другими проводами на площадь. Но выкатываясь из-под средней линии проводов, Руст заметил перед собой старую «Волгу». «Я двинулся влево, чтобы обойти ее, — рассказывал он, — и когда я это проделал, то увидел старика с таким выражением лица, как будто он не мог поверить в то, что наблюдает. Я понадеялся, что он не запаникует, не потеряет контроль над машиной и не врежется в меня».
Наконец «Сессна» прошла под последней линией проводов и выкатилась на площадь. Притормозив, Руст стал искать место для парковки. Он хотел поставить самолет на середину площади, перед Мавзолеем. Но Храм Василия Блаженного окружал небольшой забор с натянутой цепью, преграждавшей путь, поэтому «Сессна» так и осталась перед церковью.
Руст заглушил двигатель, затем на мгновение закрыл глаза и глубоко вдохнул. «Я помню это огромное чувство облегчения, как будто сбросил с себя большой груз». Он посмотрел на кремлевскую башню с часами. Было 18:43, прошло почти пять с половиной часов с тех пор, как он покинул Хельсинки. Выйдя из самолета и прислонившись к нему, пилот стал ждать штурма, однако его не последовало. Люди на Красной площади казались ошеломленными и не понимали, что происходит. Некоторые из них могли полагать, что самолет Руста был личным транспортом Горбачева или что все это — часть съемок какой-нибудь кинокартины. Но как только толпа поняла, что Руст и его «Сессна» прибыли с Запада, совершив один из самых сенсационных перелетов, свидетелями которого они все оказались, к нему приблизились.
«Вокруг меня собралась большая толпа, — говорил Руст. — Люди улыбались и подходили, чтобы пожать мне руку или попросить автограф. Там был молодой русский парень, который говорил по-английски. Он спросил меня, откуда я родом. Я сказал ему, что прибыл с Запада и хочу поговорить с Горбачевым, чтобы передать мирное послание, которое поможет ему убедить всех на Западе в том, что страна изменилась». Атмосфера была праздничной. Одна женщина дала ему кусок хлеба в знак дружбы. По словам Руста, какой-то курсант сказал ему, что «он восхищается моей инициативой, но что я должен был подать заявление на получение визы и договориться о встрече с Горбачевым, однако при этом он согласился, что меня, скорее всего, не пустили бы».
Руст не заметил, когда через толпу двинулись агенты КГБ, опрашивая людей и изымая фотоаппараты. Более чем через час после приземления прибыли два грузовика с вооруженными солдатами и грубо растолкали толпу. Они также установили барьеры вокруг самолета.
Из черного седана вышли трое мужчин и представились. Младший, переводчик, вежливо попросил у Руста паспорт и спросил, нет ли у него оружия. Затем они решили осмотреть самолет. После еще нескольких вопросов пилота попросили сесть в машину. Атмосфера встречи, по словам Руста, оставалась очень дружелюбной. «Сессну» доставили в московский международный аэропорт «Шереметьево» и разобрали для осмотра, а Руста отправили в Лефортовскую тюрьму.
Оказывается, Руст даже и не знал, что прилетел в Россию в День пограничника. Многие полагали, что он выбрал этот день, считая, что граница в это время будет менее защищенной, или, возможно, чтобы максимально досадить военным.
«Я не знал об этом, — говорит Раст. — Я сказал им: «Я западный немец. Откуда мне знать о ваших праздниках? Это было просто удачное стечение обстоятельств».
Следователи показали ему также фотографии моста, на который он приземлился. На этих снимках через мост было протянуто множество линий проводов, через которые невозможно было пробиться. Они спросили, как же прилетевший смог их все обойти? Озадаченный, Руст объяснил, что во время посадки он видел только три линии проводов. В ходе дальнейшего разбирательства выяснилось, что утром в тот день бригада рабочих временно сняла большую часть проводов из-за ремонтных работ; их заменили буквально на следующий же день. «Говорили, что я, должно быть, родился в рубашке — это русское выражение, означающее «рожденный счастливчиком», — признавался Руст. Хотя, конечно, это обстоятельство также хорошо легло в «теорию заговора».
В одном немецком периодическом издании была опубликована статья, в которой говорилось, что Руст совершил свой полет на спор. Другое издание сообщало, что он это сделал, чтобы произвести впечатление на свою девушку. Еще в одном материале говорилось, что пилот собирался разбрасывать листовки с призывом освободить из тюрьмы Рудольфа Гесса — приближенного Гитлера, все еще сидевшего тогда в тюрьме в Западном Берлине. Советская газета «Правда» объясняла странности причастностью Руста к международному заговору, в ходе которого он якобы должен был быть сбит, что спровоцировало бы новый международный конфликт. Какими бы нелепыми ни были все эти слухи, каждое такое предположение проверялось следователями, которые, впрочем, все равно закончили свою работу чрезвычайно быстро — к 23 июня 1987 года.
4 сентября, после трехдневного судебного разбирательства, коллегия из трех судей признала Руста виновным по всем пунктам обвинения и приговорила его к четырем годам заключения. Но уже 3 августа 1988 года, через два месяца после того, как Рейган и Горбачев согласовали договор о ликвидации ядерного оружия средней дальности в Европе, Верховный Совет в качестве «жеста доброй воли» решил освободить Руста из тюрьмы.
После возвращения в Германию Руст был лишен лицензии пилота, а в ноябре 1989 года, проходя альтернативную службу в больнице, серьезно ранил ножницами медсестру за то, что она отказалась проявить к нему благосклонность. За это его приговорили к 2,5 годам лишения свободы, освободив спустя 15 месяцев. Затем он торговал обувью, жертвуя деньги детскому дому, а в 1994 году вновь поехал в Россию, пытаясь встретиться с Горбачевым и организовать свою фирму — но ни то, ни другое ему не удалось. В 1997 году обратился в индуизм и женился на индийской девушке по имени Гита, дочери торговца чаем из Бомбея, но позже с ней развелся. В апреле 2001 года Руст снова предстал перед судом — на этот раз его обвинили в краже кашемирового свитера в универмаге и приговорили к большому штрафу, который впоследствии уменьшили. Еще одно столкновение с законом у него случилось в 2005 году: он был признан виновным в мошенничестве, но отделался штрафом в размере €1500. На жизнь он зарабатывал также игрой в покер, преподаванием йоги и, по его собственным словам, работая в качестве аналитика инвестиционного банка в Цюрихе.