Организация процесса тестирования

Как мы уже упоминали в начале главы, тестирование - довольно-таки обширная тема, в которой существует несогласованность в терминологии и методологии. В Rust-сообществе принято говорить о тестировании в как модульном (unit tests) и интеграционном (integration tests). Модульные тесты - это короткие, изолированные по среде выполнения и времени тесты. Данные тесты могут проверять работу внутренний (private) частей модулей. Интеграционные тесты - это тестирование функционала вашей библиотеки при взаимодействии с другими, внешними контейнерами. В данных тестах можно использовать только открытие (pub) функционал, чтобы проверить его работу, как внешний компонент.

Реализация всех этих типов тестов важна для понимания надежности работы компонент, как изолировано, так и совместно с внешними контейнерами.

Модульные тесты

Целью модульного тестирования является изолированное от остального функционала проекта малой части программы, чтобы можно было быстро понять, что не работает корректно. Мы сохраняем такие тесты в папку src, там же где и тестируемый функционал. В Rust принято называть тестирующий модуль tests и его код сохранять в тот же файл, что предстоит тестировать. Также необходимо добавить аннотацию cfg(test) к этому модулю.

Использование аннотации #[cfg(test)]

Аннотация #[cfg(test)] тестирующего модуля сообщает компилятору, что необходимо компилировать и тестировать по команде cargo test. Это позволяет экономить время на компиляцию, когда необходимо только лишь компиляция (без тестирования). При обычной компиляции код тестирующего модуля не входит в компилируемый код. Т.к. интеграционные тесты не входят в состав файла тестируемого функционала код таких тестов не нуждается в этой специальной аннотации.

Как вы, конечно, помните при создание нового проекта библиотеки в файл исходного кода добавляется код тестирующего модуля:

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
    }
}
#}

Атрибут cfg - это сокращение от слова конфигурация (configuration). Он сообщает компилятору, что аннотируемый модуль необходимо включать в только лишь при соблюдении определенных в конфигурации условий (в данном случае - это условие test). Т.е. если carogo будет запущен с опцией test компилятор будет работать с этой частью кода.

Тестирования функций с ограниченной областью видимости (private)

Сообщество программистов не имеет однозначного мнения по поводу способа тестирования закрытых (private) функций. В некоторых языках весьма сложно или даже невозможно тестировать такие функции. В Rust закрытые функции. Рассмотрим пример (11-12) тестирования закрытой функции:

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
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), но так как тестирующий модуль имеет возможность импортировать код use super::*; тест скомпилируется и выполниться без каких-либо проблем. Если же вы считает, что закрытые функции не должны быть тестируемы - Rust также не будет вас в этом ограничивать, сопровождая предупреждениями компиляцию кода.

Интеграционные тесты

Интеграционные тесты в Rust должны быть внешними относительно тестируемой библиотеки. Тесты моделируют различные сценарии использования библиотеки. Идет взаимодействие с помощью открытых интерфейсов. Целью данного тестировать проверить слаженность выполнения сценариев работы функционалом библиотеки в целом. Коды интеграционных тестов храниться в папке проекта tests.

Содержание папки tests

Для создания интеграционных тестов вам понадобиться создать папку tests в корневой папке вашего проекта (в той же, где находится папка src). По условиям конвенций Cargo интеграционные файлы с тестами будут храниться в этой директории. Каждый такой файл будет компилироваться в отдельный контейнер (crate).

Начнем практику. В корневой папке проекта adder создадим папку tests. Далее создадим файл tests/integration_test.rs и в него внесём следующий код (11-13):

Filename: tests/integration_test.rs

extern crate adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

Код программы 11-13: Интеграционный тест контейнера adder

Обратите внимание, что мы добавили дополнительную информацию для компилятора в для теста extern crate adder, которая для модульного теста не нужна. Т.к. при тестировании происходит моделирование изоляции при доступе к тестируемому функционалу, а физически кода тестов и исходные коды разделены по разным папкам, описания тестируемого контейнера необходимо.

Также обратите внимание, что нам не нужно добавлять конфигурационный атрибут в аннотацию тестов. Cargo считает директорию tests специальной и данные тесты будут компилироваться, только в случае запуска команды тестирования cargo test. Убедимся на практике:

cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running target/debug/deps/adder-abcabcabc

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

     Running target/debug/deps/integration_test-ce99bcc2479f4607

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

Обратите внимание, что интеграционные тесты будут запускаться только если модульные тесты успешно сработали. Если в них будут ошибки. Интеграционные тесты не будут запущены.

Подведем промежуточные итоги. При запуске команды тестирования, по умолчанию, у нас есть три секции вывода: секция модульных тестов, секция интеграционных тестов и секция тестов документации. В первой секции описание прохождения модульных тестов следуют один за одним.

В секции интеграционных тестов мы видим, что воздаются файлы для каждого теста и мы видим их названия. Далее идёт сводное описание работы интеграционных тестов.

Каждый интеграционный тест имеет свою секцию описания. Т.е. чем больше будет интеграционных тестов, тем больше будет секций описания.

Мы также можем запускать интеграционные тесты по имени. Для запуска всех тестов в выбранном файле интеграционных тестов, используйте аргумент --test команды cargo test. Например:

$ cargo test --test integration_test
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running target/debug/integration_test-952a27e0126bb565

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

Эта команда запускает только те тесты, которые содержаться в файле integration_test.rs в папке tests.

Подмодули в интеграционных тестах

При росте числа интеграционных тестов, естественным путём оптимизации будет разделение тесты на отдельные файлы.

Реализация каждого интеграционного теста, как одного модуля оправдано с точки зрения реализации изоляции. Недостатком такого разделения является наложение ограничений на возможности общего доступа к функциям, структурам и пр. между файлами тестов. К примеру, если вы создадите файл tests/common.rs и создадите в ней функцию setup, то чтобы иметь доступ к этой функции из другого файла интеграционных тестов, то вам придётся разместить кода данной функции в других файлах (копированием и вставкой):

Filename: tests/common.rs


# #![allow(unused_variables)]
#fn main() {
pub fn setup() {
    // setup code specific to your library's tests would go here
}
#}

Если вы запустите команду запуска тестирования снова, то вы увидите новую секцию для файла common.rs, даже если файла не содержать тестов:

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

     Running target/debug/deps/common-b8b07b6f1be2db70

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

     Running target/debug/deps/integration_test-d993c68b431d39df

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

Наличие секции common и текстов типа running 0 tests не то, чтобы мы хотели. Необходимо иметь доступ всем интеграционным файлам к этой функции.

Для этой цели можно создать модуль tests/common/mod.rs. Когда вы перенесёте функцию setup это файл, секцию, которую мы видели при предыдущем запуске тестирования (common...) не будет показана. Файлы в поддиректории папки tests не компилируются, как отдельные контейнеры.

После того, как вы создали модуль tests/common/mod.rs мы можем использовать содержание его функций в любых интеграционный файлах. Создадим тест и испльзуем функцию setup в файле tests/integration_test.rs:

Filename: tests/integration_test.rs

extern crate adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

Обратите внимание на ссылку на модуль mod common;. Далее, в коде теста вы можете получить доступ к функции common::setup().

Интеграционные тесты для бинарных контейнеров

Если наши проект является бинарным контейнером и содержать только src/main.rs и не содержит src/lib.rs мы не можем создать интеграционные тесты в папке tests и использовать extern crate для импорта функций объявленных в файле src/main.rs. Только библиотечные контейнеры могут быть использованы для хранения кода, к которому можно получить доступ. Бинарные контейнеры могу запущены на выполнение.

Поэтому, основной код программной логики контейнера удобно хранить в файле src/lib.rs. Код в файле src/main.rs содержать только запускающие функции и минимальную логику, которую не надо тестировать ввиду её очевидной простоты

Итоги

Возможности тестирования в Rust предполагают определенный стиль написания тестов для создаваемых модулей. Существуют два типа тестирования, которые имеют разные задачи и поэтому реализуются различным образом. Тестирования является важным элементом жизненного цикла программного обеспечения и разработчика Cargo знают об этом и предлагают рациональные решения этой задачи.

Пожалуйста, объедините полученные знания и приступайте. В следующей главе и будете использовать знания и навыки накопленные за время учёбы Rust.