Подтвердите поведение с помощью тестов Rust

RustBeginner
Практиковаться сейчас

Введение

Тесты превращают ожидание в проверяемое свидетельство. Целевой тест проверяет одно поведение, сравнивает фактический результат с ожидаемым и делает будущие изменения безопаснее, автоматически обнаруживая регрессии.

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

Добавьте целевой модульный тест

На этом шаге вы разместите модульный тест рядом с кодом библиотеки и запустите только тест с указанным именем.

Проект находится в /home/labex/project/shipping-quote. Перейдите в него:

cd /home/labex/project/shipping-quote

Перед редактированием просмотрите короткий исходный код библиотеки. Здесь команда sed -n '1,220p' используется только для чтения: параметр -n отключает автоматический вывод, а 1,220p явно выводит строки с 1 по 220. Этого диапазона достаточно, чтобы показать весь файл без открытия редактора:

sed -n '1,220p' src/lib.rs

Синтаксис вида #[...] называется атрибутом — это метаданные, прикреплённые к следующему элементу Rust. Атрибут #[cfg(test)] указывает Rust компилировать следующий тестовый модуль только при сборке тестов. Модуль — это именованный контейнер для связанных элементов; в следующем Guided Lab модули рассматриваются как общий инструмент организации кода.

Внутри mod tests слово пути super означает родительский модуль, а * — все доступные имена из этого родительского модуля. Поэтому use super::*; делает функции библиотеки доступными внутри тестового модуля по коротким именам. Наконец, функция с атрибутом #[test] становится тестом, который Cargo может обнаружить и запустить.

Откройте исходный файл в Nano:

nano src/lib.rs

Замените // FIRST_UNIT_TEST следующим небольшим тестом:

    #[test]
    fn light_parcel_costs_five() {
        assert_eq!(shipping_cost(3), 5);
    }

Макрос assert_eq! сравнивает два выражения. Если они равны, тест продолжается; если нет, тест завершается с ошибкой и выводит оба значения. В данном случае тест фиксирует, что доставка посылки массой три килограмма стоит пять единиц.

Сохраните файл с помощью Ctrl+O, нажмите Enter, а затем выйдите с помощью Ctrl+X. Передайте имя теста после cargo test, чтобы запустить только тесты, имена которых содержат этот текст:

cargo test light_parcel_costs_five

Cargo скомпилирует тестовую сборку и выведет результат указанного модульного теста:

test tests::light_parcel_costs_five ... ok

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

Префикс tests:: показывает, что тест находится внутри локального тестового модуля библиотеки, а ok подтверждает успешное выполнение утверждения.

Проверьте граничные случаи

На этом шаге вы добавите тесты для границ, в которых меняется поведение доставки.

Такое обычное значение, как 3, проверяет одну точку внутри диапазона для лёгкой посылки, но ошибки часто скрываются на границах. В подготовленном match запись 1..=5 — это шаблон диапазона, который соответствует любому значению от одного до пяти включительно, включая обе границы. Здесь обозначение включительного диапазона, знакомое по циклам, используется в новом контексте — в шаблоне. Функция обрабатывает нулевую массу особым образом и сохраняет тариф в пять единиц ровно до пяти килограммов включительно. Проверка этих значений фиксирует обе границы.

Снова откройте библиотеку:

nano src/lib.rs

Замените // EDGE_UNIT_TESTS двумя небольшими тестами:

    #[test]
    fn empty_parcel_is_free() {
        assert_eq!(shipping_cost(0), 0);
    }

    #[test]
    fn five_kilograms_is_still_light() {
        assert_eq!(shipping_cost(5), 5);
    }

Имя каждого теста описывает одно правило, а сам тест содержит одно целевое утверждение. Благодаря этому легко определить причину сбоя. Сохраните файл и выйдите из Nano.

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

cargo test tests::

Надёжным подтверждением будут три успешно пройденных модульных теста:

test tests::empty_parcel_is_free ... ok
test tests::five_kilograms_is_still_light ... ok
test tests::light_parcel_costs_five ... ok

test result: ok. 3 passed; 0 failed; ...

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

Завершите интеграционный тест

На этом шаге вы протестируете библиотеку через её открытый интерфейс из отдельного файла.

Модульные тесты внутри src/lib.rs находятся рядом с реализацией и могут использовать внутренние элементы родительского модуля. Интеграционные тесты находятся в каталоге верхнего уровня tests/, компилируются как отдельные тестовые программы и используют только публичный интерфейс библиотеки, как внешний вызывающий код. Имя пакета shipping-quote становится именем крейта Rust shipping_quote с символом подчёркивания в исходном коде. В следующем Lab закрепляются термины «пакет», «крейt», «модуль» и «публичная видимость».

Просмотрите подготовленный шаблон интеграционного теста:

sed -n '1,160p' tests/order_workflow.rs

Он импортирует только публичную функцию order_total, вызывает её для заказа стоимостью 40 единиц и посылки массой три килограмма, а затем оставляет одно утверждение для вас. Откройте файл:

nano tests/order_workflow.rs

Замените строку с утверждением-заполнителем, заканчивающуюся комментарием // INTEGRATION_ASSERTION, на:

    assert_eq!(total, 45);

Ожидаемая сумма складывается из промежуточной суммы в 40 единиц и стоимости доставки лёгкой посылки в пять единиц. Сохраните файл и выйдите из Nano.

Параметр --test order_workflow выбирает цель интеграционного тестирования, исходный файл которой находится в tests/order_workflow.rs:

cargo test --test order_workflow

Цель выведет результат теста публичного сценария:

test order_total_includes_shipping ... ok

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

В конце запустите весь набор тестов пакета без фильтра:

cargo test

Cargo запустит три модульных теста и интеграционный тест. Отдельные сводки результатов нормальны, поскольку это разные тестовые бинарные файлы; у каждого именованного теста должно отображаться ok, а в каждой сводке должно быть ноль ошибок.

Итоги

Вы добавили локальные модульные тесты, проверили границы правил, завершили интеграционный тест через публичный API библиотеки и использовали фильтры Cargo для запуска одного теста, одного модуля, одной интеграционной цели или полного набора. Эти приёмы дают быстрое подтверждение во время разработки и обеспечивают более широкую защиту от регрессий перед передачей результата.