Для запуска юнит-тестов в Rust в общем случае применяется команда:
cargo test
При выполнении этой команды cargo, менеджер пакетов Rust, идентифицирует тесты и запускает их. Cargo собирает результаты и представляет сводку, в которой указывается, какие тесты прошли, а какие нет.
Однако стоит отметить, что эта команда имеет некоторые ограничения. Прежде всего, она запускает тесты, которые определены в главном файле приложения - в файле main.rs. Даже
если в этом файле нет никаких тестов, то команда cargo test все равно просматривает его. Именно поэтому для простоты мы и определяли в прошлой теме весь функционал - и функцию, и тест к ней - в файле main.rs.
Но, естественно, это не самый предпочтительный подход, особенно когда мы хотим отделить тесты от функционала программы, не говоря о том, что сами тестируемые функции могут быть разбросаны по всему приложению в разных файлах, модулях, пакетах. В
жтом случае мы можем подключать модуль, где определен тест. Например, пусть у нас проекте также есть файл addition.rs со следующим кодом:
pub fn add(a: i32, b: i32) -> i32 { // функция для тестирования
a + b
}
#[test]
fn test_add() {
let input_1 = 2;
let input_2 = 8;
let result = add(input_1, input_2);
assert_eq!(result, 10, "The addition result is incorrect.");
}
Здесь, как и в прошлой теме, определена функция add() для сложения двух чисел и тест test_add() для этой функции. В файл main.rs подключим этот файл:
mod addition; // подключаем модуль addition
fn main() {
println!("Hello, World");
}
В функции main нам необязательно использовать функционал модуля addition, например, функцию add(), достаточно просто подключить модуль. И далее запустим команду cargo test
eugene@Eugene:~/Documents/rust/testapp$ cargo test
Compiling testapp v0.1.0 (/home/eugene/Documents/rust/testapp)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s
Running unittests src/main.rs (target/debug/deps/unit-cee7686df844452f)
running 1 test
test addition::test_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
eugene@Eugene:~/Documents/rust/testapp$
Организация тестов в отдельные модули упрощает управление тестами, особенно по мере роста кодовой базы. Для создания модуля тестов, как и в общем случае, применяется ключевое слово
mod. При этом при определении модулей тестов рекомендуется использовать атрибут #[cfg(test)]. Этот атрибут указывает Rust компилировать и
запускать тестовый код только при запуске команды cargo test, а не при запуске проекта. Это экономит время компиляции, когда вам нужно только собрать библиотеку,
и экономит место в скомпилированной сборке, поскольку тесты в нее не включаются.
Итак, изменим файл addition.rs следующим образом:
pub fn add(a: i32, b: i32) -> i32 { // функция для тестирования
a + b
}
#[cfg(test)]
mod tests {
use super::*; // подключаем окружающую функциональность
#[test]
fn test_add_positive() {
let input_1 = 2;
let input_2 = 8;
let result = add(input_1, input_2);
assert_eq!(result, 10, "The addition result is incorrect.");
}
#[test]
fn test_add_negative() {
let input_1 = -2;
let input_2 = -8;
let result = add(input_1, input_2);
assert_eq!(result, -10, "The negative addition result is incorrect.");
}
}
Для примера я добавил здесь второй тест - test_add_negative() для проверки сложения отрицательных чисел. При этом оба теста определены в отдельном модуле, то есть они располагаются в модуле addition:tests.
Для подключения окружающей их функциональности - к функции add применяется выражение use super::*;
Запустим тесты:
eugene@Eugene:~/Documents/rust/testapp$ cargo test
Compiling testapp v0.1.0 (/home/eugene/Documents/rust/testapp)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.08s
Running unittests src/main.rs (target/debug/deps/testapp-cee7686df844452f)
running 2 tests
test addition::tests::test_add_positive ... ok
test addition::tests::test_add_negative ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
eugene@Eugene:~/Documents/rust/testapp$
Таким образом, было запущено два теста, и оба теста выполнились успешно.
Эта команда также предоставляет несколько опций и функций для управления выполнением тестов. Один из распространенных сценариев — запуск тестов с указанием их имен. Для этого можно использовать следующий синтаксис:
cargo test test_fn_name
В данном случае "test_fn_name" - это имя теста, который надо запустить. Эта опция особенно полезна, если надо выполнить не все тесты, а лишь некоторые.
В случаях, когда указанное имя test_fn_name соответствует нескольким тестам, cargo выполнит их все. Однако могут возникнуть ситуации, когда вам нужно запустить только один конкретный тест,
и для этого можно добавить опцию --exact в качестве аргумента:
cargo test test_fn_name -- --exact
Флаг --exact гарантирует, что cargo запустит только один тест, который точно соответствует указанному имени test_fn_name, что помогает избежать непреднамеренного многократного
выполнения.
Однако если функции тестов находятся внутри модулей, необходимо указать полный путь к тесту с учетом пространства имен. Это можно сделать с помощью следующей команды:
cargo test test_mod_name::test_fn_name -- --exact
Здесь предполагается что функция теста "test_fn_name" располагается в модуле "test_mod_name".
Если функции тестов располагаются в определенных пакетах, то с помощью опции --package можно указать имя пакета:
cargo test --package package_name test_fn_name -- --exact cargo test --package package_name test_mod_name::test_fn_name -- --exact
Например, выполним выше определенный тест test_add_negative():
eugene@Eugene:~/Documents/rust/testapp$ cargo test test_add_negative
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/main.rs (target/debug/deps/unit-cee7686df844452f)
running 1 test
test addition::tests::test_add_negative ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
eugene@Eugene:~/Documents/rust/testapp$
Макросы assert (утверждения) позволяют проверить различные ситуации и верифицировать, что тест прошел проверку или нет. В Rust стандартная библиотека предоставляет множество макросов assert, каждый из которых предназначен для разных сценариев. Приведу лишь наиболее часто используемые макросы:
assert!(condition): проверяет истинность условия. Если условие НЕ верно, то тест не пройден. Условие представляет любой выражение, которое возвращает true или false.
assert!(expression, "текст сообщения"): аналогичен предыдущему макросу, только в качестве последнего параметра позволяет задать сообщение, которое будет выводиться, если тест не пройден
assert_eq!(left, right): проверяет, что левый операнд равен правому. Если они не равны, тест не пройден
assert_eq!(left, right, "текст сообщения"): аналогичен предыдущему, только также позволяет задать сообщение, которое будет выводиться, если тест не пройден
assert_ne!(left, right): проверяет, что левый операнд НЕ равен правому. Если они равны, тест не пройден
assert_ne!(left, right, "текст сообщения"): аналогичен предыдущему, только также позволяет задать сообщение, которое будет выводиться, если тест не пройден
Пример применения функций:
#[test]
fn test_is_even() {
assert_eq!(is_even(2), true); // проверяем на равенство
assert_ne!(is_even(3), true); // проверяем на неравенство
assert!(is_even(4)); // проверяем истинность условия
}
fn is_even(n: i32) -> bool { n % 2 == 0 }
Здесь определена функция is_even(), которая принимает число и возвращает true, если это число четное, и false, если нечетное. И с помощью макросов assert
проверяем различные ситуации.
Выполнение модульных тестов нередко требует некоторой настройки окружения. Например, бывает необходимо перед выполнение тестов создать определенные условия или выделить ресурсы. Более того, может возникнуть необходимость использовать одни и те же настройки сразу в нескольких тестах, чтобы обеспечить единообразие и согласованность сценариев тестирования. Для этого можно создать функцию настройки, которая создает условное окружение для тестов. Например:
use std::collections::HashMap;
// настраиваем в качестве окружения HashMap
fn create_env() -> HashMap<String, u32> {
let mut env = HashMap::new();
env.insert("foo".to_string(), 44);
env.insert("bar".to_string(), 39);
env
}
#[test]
fn test_contains_foo() {
let env = create_env();
assert!(env.contains_key("foo")); // проверяем наличие в словаре ключа "foo"
}
#[test]
fn test_contains_bar() {
let env = create_env();
assert!(env.contains_key("bar")); // проверяем наличие в словаре ключа "bar"
}
#[test]
fn test_contains_baz() {
let env = create_env();
assert!(!env.contains_key("baz")); // проверяем отсутствие в словаре ключа "baz"
}
Здесь в качестве среды окружения для тестов выступает объект HashMap. И тесты проверяют наличие или отсутствие в нем определенных ключей.