Лабораторная работа по книге Rust: Юнит-тесты и интеграционные тесты

Beginner

This tutorial is from open-source community. Access the source code

Введение

Добро пожаловать в Test Organization. Этот лабиринт является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

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

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

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

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

Юнит-тесты

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

Модуль tests и #[cfg(test)]

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

Помните, что когда мы создавали новый проект adder в первом разделе этой главы, Cargo сгенерировал для нас этот код:

Имя файла: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

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

Тестирование приватных функций

В сообществе тестирования существует дискуссия о том, следует ли тестировать приватные функции напрямую, и в других языках это может быть затруднительно или невозможно. Независимо от того, какому идеологии тестирования вы придерживаетесь, правила приватности Rust позволяют вам тестировать приватные функции. Рассмотрим код в Listing 11-12 с приватной функцией internal_adder.

Имя файла: src/lib.rs

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));
    }
}

Listing 11-12: Тестирование приватной функции

Обратите внимание, что функция internal_adder не помечена как pub. Тесты - это просто Rust-код, а модуль tests - это просто другой модуль. Как мы обсуждали в разделе "Пути для обращения к элементу в дереве модулей", элементы в дочерних модулях могут использовать элементы в их предках. В этом тесте мы подтягиваем все элементы родителя модуля test в область видимости с помощью use super::*, и затем тест может вызвать internal_adder. Если вы не считаете, что приватные функции должны быть тестированы, в Rust ничего не заставит вас это делать.

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

В Rust интеграционные тесты находятся полностью вне вашей библиотеки. Они используют вашу библиотеку так же, как и любой другой код, что означает, что они могут вызывать только те функции, которые являются частью публичного API вашей библиотеки. Их цель - проверить, правильно ли взаимодействуют многие части вашей библиотеки. Фрагменты кода, которые работают корректно отдельно, могут иметь проблемы при интеграции, поэтому покрытие тестами интегрированного кода также имеет важное значение. Чтобы создать интеграционные тесты, вам сначала нужно создать директорию tests.

Директория tests

Мы создаем директорию tests в верхнем уровне директории нашего проекта, рядом с src. Cargo знает, что нужно искать файлы интеграционных тестов в этой директории. Затем мы можем создать столько тестовых файлов, сколько захотим, и Cargo скомпилирует каждый файл как отдельный крейт.

Давайте создадим интеграционный тест. С кодом из Listing 11-12 по-прежнему в файле src/lib.rs, создайте директорию tests и новый файл с именем tests/integration_test.rs. Структура вашей директории должна выглядеть так:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

Вставьте код из Listing 11-13 в файл tests/integration_test.rs.

Имя файла: tests/integration_test.rs

use adder;

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

Listing 11-13: Интеграционный тест функции в крейте adder

Каждый файл в директории tests является отдельным крейтом, поэтому нам нужно подтянуть нашу библиотеку в область видимости каждого тестового крейта.出于这个原因,我们在代码顶部添加了 use adder;,这在单元测试中是不需要的。

В файле tests/integration_test.rs мы не нужно аннотировать никакой код с помощью #[cfg(test)]. Cargo особым образом обрабатывает директорию tests и компилирует файлы в этой директории только при запуске cargo test. Теперь запустите cargo test:

[object Object]

Три раздела вывода включают в себя юнит-тесты, интеграционные тесты и тесты документации. Обратите внимание, что если любой тест в разделе не пройдет, последующие разделы не будут запущены. Например, если юнит-тест не пройдет, не будет вывода для интеграционных и тестов документации, потому что эти тесты будут запущены только в том случае, если все юнит-тесты пройдут.

Первый раздел для юнит-тестов [1] такой же, как мы уже видели: одна строка для каждого юнит-теста (один назван internal, который мы добавили в Listing 11-12), а затем обобщающая строка для юнит-тестов.

Раздел интеграционных тестов начинается со строки Running tests/integration_test.rs [2]. Затем идет одна строка для каждой тестовой функции в этом интеграционном тесте [3] и обобщающая строка для результатов интеграционного теста [4] сразу перед началом раздела Doc-tests adder.

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

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

[object Object]

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

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

По мере добавления большего количества интеграционных тестов вы, возможно, захотите создать больше файлов в директории tests, чтобы упростить их организацию; например, можно сгруппировать тестовые функции по функциональности, которую они тестируют. Как уже упоминалось ранее, каждый файл в директории tests компилируется как отдельный крейт, что полезно для создания отдельных областей видимости, чтобы более точно имитировать способ использования вашего крейта конечными пользователями. Однако, это означает, что файлы в директории tests не обладают тем же поведением, что и файлы в src, как вы узнали в главе 7 о том, как разделить код на модули и файлы.

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

Имя файла: tests/common.rs

pub fn setup() {
    // здесь должен быть код настройки, специфичный для тестов вашей библиотеки
}

Когда мы снова запускаем тесты, мы увидим новый раздел в выводе тестов для файла common.rs, хотя в этом файле нет никаких тестовых функций, и мы не вызывали функцию setup ни откуда:

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 также понимает, о котором мы говорили в разделе "Альтернативные пути к файлам". Такое именование файла сообщает Rust не рассматривать модуль common как файл интеграционного теста. Когда мы переносим код функции setup в tests/common/mod.rs и удаляем файл tests/common.rs, соответствующий раздел в выводе тестов больше не появится. Файлы в поддиректориях директории tests не компилируются как отдельные крейты и не появляются в выводе тестов.

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

Имя файла: tests/integration_test.rs

use adder;

mod common;

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

Обратите внимание, что объявление mod common; такое же, как и объявление модуля, которое мы демонстрировали в Listing 7-21. Затем, в тестовой функции, мы можем вызвать функцию common::setup().

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

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

Это одна из причин, по которой Rust-проекты, которые предоставляют бинарный файл, имеют простой файл src/main.rs, который вызывает логику, хранящуюся в файле src/lib.rs. Используя такую структуру, интеграционные тесты могут проверить библиотечный крейт с использованием use, чтобы сделать доступной важную функциональность. Если важная функциональность работает, небольшой объем кода в файле src/main.rs также будет работать, и этот небольшой объем кода не требует тестирования.

Резюме

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