Определение и запуск юнит-тестов

Последнее обновление: 20.05.2024

Для запуска юнит-тестов в 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$ 

Модули тестов и атрибут #[cfg(test)]

Организация тестов в отдельные модули упрощает управление тестами, особенно по мере роста кодовой базы. Для создания модуля тестов, как и в общем случае, применяется ключевое слово 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

Макросы 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. И тесты проверяют наличие или отсутствие в нем определенных ключей.

Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850