Как писать тесты

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

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

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

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

В этом лабе мы узнаем, как писать тесты на Rust с использованием атрибутов, макросов и утверждений.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/ErrorHandlingandDebuggingGroup(["Error Handling and Debugging"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/ErrorHandlingandDebuggingGroup -.-> rust/panic_usage("panic! Usage") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100415{{"Как писать тесты"}} rust/integer_types -.-> lab-100415{{"Как писать тесты"}} rust/string_type -.-> lab-100415{{"Как писать тесты"}} rust/function_syntax -.-> lab-100415{{"Как писать тесты"}} rust/expressions_statements -.-> lab-100415{{"Как писать тесты"}} rust/method_syntax -.-> lab-100415{{"Как писать тесты"}} rust/panic_usage -.-> lab-100415{{"Как писать тесты"}} rust/traits -.-> lab-100415{{"Как писать тесты"}} end

Как писать тесты

Тесты — это функции Rust, которые проверяют, работает ли некорректный код в ожидаемом режиме. Тело тестовых функций обычно выполняет эти три действия:

  • Подготавливает любые необходимые данные или состояние.
  • Запускает код, который вы хотите протестировать.
  • Утверждает, что результаты соответствуют вашим ожиданиям.

Рассмотрим функции Rust, которые обеспечивают запись тестов, которые выполняют эти действия, в том числе атрибут test, несколько макросов и атрибут should_panic.

Структура тестовой функции

В самом простом случае тест в Rust — это функция, помеченная атрибутом test. Атрибуты — это метаданные о частях кода Rust; примером является атрибут derive, который мы использовали с структурами в главе 5. Чтобы превратить функцию в тестовую функцию, добавьте #[test] на строке перед fn. Когда вы запускаете свои тесты с помощью команды cargo test, Rust создает бинарный файл тестового исполнителя, который запускает помеченные функции и сообщает, пройден ли каждый тестовый метод или нет.

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

Мы рассмотрим некоторые аспекты работы тестов, экспериментируя с шаблонным тестом, прежде чем на самом деле протестировать какой-либо код. Затем мы напишем несколько реальных тестов, которые вызовут какой-то код, который мы написали, и проверим, что его поведение корректно.

Создадим новый библиотечный проект под названием adder, который будет складывать два числа:

$ cargo new adder --lib
Created library $(adder) project
$ cd adder

Содержимое файла src/lib.rs в библиотеке adder должно выглядеть, как показано в Листинге 11-1.

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

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

Листинг 11-1: Автоматически сгенерированный тестовый модуль и функция при использовании cargo new

На данный момент暂且 игнорируем первые две строки и сосредоточимся на функции. Обратите внимание на аннотацию #[test] [1]: этот атрибут указывает, что это тестовая функция, поэтому тестовый исполнитель знает, что нужно рассматривать эту функцию как тест. В модуле tests могут быть и другие не тестовые функции, которые помогают настроить общие сценарии или выполнять общие операции, поэтому мы всегда должны указывать, какие функции являются тестами.

Тело примера функции использует макрос assert_eq! [2], чтобы проверить, что result, содержащий результат сложения 2 и 2, равен 4. Эта проверка служит примером формата для типичного теста. Запустим его, чтобы убедиться, что этот тест пройден.

Команда cargo test запускает все тесты в нашем проекте, как показано в Листинге 11-2.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-
92948b65e88960b4)

1 running 1 test
2 test tests::it_works... ok

3 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

  4 Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Листинг 11-2: Вывод при запуске автоматически сгенерированного теста

Cargo скомпилировал и запустил тест. Мы видим строку running 1 test [1]. Следующая строка показывает имя сгенерированной тестовой функции, называемой it_works, и то, что результат выполнения этого теста — ok [2]. Общая сводка test result: ok. [3] означает, что все тесты пройдены, а часть, которая говорит 1 passed; 0 failed, показывает количество пройденных или не пройденных тестов.

Возможно, пометить тест как игнорируемый, чтобы он не запускался в определенном случае; мы поговорим об этом в разделе "Игнорирование некоторых тестов, если не требуется специально". Поскольку мы этого не сделали здесь, сводка показывает 0 ignored. Мы также можем передать аргумент команде cargo test, чтобы запустить только тесты, имена которых соответствуют строке; это называется фильтрацией, и мы поговорим об этом в разделе "Запуск части тестов по имени". Здесь мы не фильтровали тесты, которые запускаются, поэтому в конце сводки показывается 0 filtered out.

Статистика 0 measured относится к бенчмарк-тестам, которые измеряют производительность. Бенчмарк-тесты на момент написания доступны только в nightly-версии Rust. См. документацию по бенчмарк-тестам по адресу https://doc.rust-lang.org/unstable-book/library-features/test.html, чтобы узнать больше.

Следующая часть вывода теста, начиная с Doc-tests adder [4], относится к результатам любых тестов по документации. У нас еще нет тестов по документации, но Rust может компилировать любые примеры кода, которые появляются в нашей API-документации. Эта функция помогает поддерживать синхронизацию между вашими документами и кодом! Мы поговорим о том, как писать тесты по документации, в разделе "Комментарии по документации как тесты". На данный момент мы暂且 игнорируем вывод Doc-tests.

Давайте начнем настраивать тест под свои нужды. Во - первых, измените имя функции it_works на другое, например, exploration, вот так:

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

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

Затем снова запустите cargo test. Теперь вывод показывает exploration вместо it_works:

running 1 test
test tests::exploration... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Теперь добавим еще один тест, но на этот раз создадим тест, который не пройдет! Тесты не проходят, когда что - то в тестовой функции вызывает панику. Каждый тест запускается в отдельном потоке, и когда главный поток видит, что тестовый поток завершился с ошибкой, тест помечается как не пройденный. В главе 9 мы говорили, что самый простой способ вызвать панику — это вызвать макрос panic!. Введите новый тест в виде функции с именем another, чтобы файл src/lib.rs выглядел, как показано в Листинге 11-3.

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

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

Листинг 11-3: Добавление второго теста, который не пройдет, потому что мы вызываем макрос panic!

Запустите тесты снова с помощью cargo test. Вывод должен выглядеть, как показано в Листинге 11-4, где показано, что наш тест exploration пройден, а another не пройден.

running 2 tests
test tests::exploration... ok
1 test tests::another... FAILED

2 failures:

---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

3 failures:
    tests::another

4 test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Листинг 11-4: Результаты теста, когда один тест пройден, а другой не пройден

Вместо ok строка test tests::another показывает FAILED [1]. Между отдельными результатами и сводкой появляются два новых раздела: первый [2] показывает подробную причину неудачи каждого теста. В этом случае мы получаем подробности, что another не пройден, потому что он panicked at 'Make this test fail' на строке 10 в файле src/lib.rs. Следующий раздел [3] перечисляет только имена всех не пройденных тестов, что полезно, когда есть много тестов и много подробного вывода о неудачных тестах. Мы можем использовать имя не пройденного теста, чтобы запустить только этот тест, чтобы更容易 отлаживать его; мы поговорим больше о способах запуска тестов в разделе "Управление запуском тестов".

Строка сводки отображается в конце [4]: в целом, наш результат теста — FAILED. У нас был один пройденный тест и один не пройденный тест.

Теперь, когда вы видели, как выглядят результаты тестов в разных сценариях, давайте рассмотрим некоторые макросы, кроме panic!, которые полезны в тестах.

Проверка результатов с помощью макроса assert!

Макрос assert!, предоставляемый стандартной библиотекой, полезен, когда вы хотите убедиться, что какое - то условие в тесте оценивается как true. Мы передаем макросу assert! аргумент, который оценивается в булево значение. Если значение равно true, ничего не происходит и тест проходит. Если значение равно false, макрос assert! вызывает panic!, чтобы привести к неудаче тест. Использование макроса assert! помогает нам проверить, работает ли наш код так, как мы предполагаем.

В Листинге 5-15 мы использовали структуру Rectangle и метод can_hold, которые повторяются здесь в Листинге 11-5. Давайте поместим этот код в файл src/lib.rs, а затем напишем для него несколько тестов с использованием макроса assert!.

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

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Листинг 11-5: Использование структуры Rectangle и ее метода can_hold из главы 5

Метод can_hold возвращает булево значение, что означает, что это идеальный случай для использования макроса assert!. В Листинге 11-6 мы пишем тест, который проверяет метод can_hold, создав экземпляр структуры Rectangle с шириной 8 и высотой 7 и проверив, может ли он вместить другой экземпляр структуры Rectangle с шириной 5 и высотой 1.

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

#[cfg(test)]
mod tests {
  1 use super::*;

    #[test]
  2 fn larger_can_hold_smaller() {
      3 let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

      4 assert!(larger.can_hold(&smaller));
    }
}

Листинг 11-6: Тест для can_hold, который проверяет, может ли большая прямоугольная область вместить меньшую

Обратите внимание, что мы добавили новую строку внутри модуля tests: use super::*; [1]. Модуль tests — это обычный модуль, который подчиняется обычным правилам видимости, о которых мы говорили в разделе "Пути для обращения к элементу в дереве модулей". Поскольку модуль tests является внутренним модулем, нам нужно подключить код, подлежащий тестированию, из внешнего модуля в область видимости внутреннего модуля. Мы используем глобальную ссылку здесь, поэтому все, что мы определяем в внешнем модуле, доступно для этого модуля tests.

Мы назвали наш тест larger_can_hold_smaller [2], и создали два экземпляра структуры Rectangle, которые нам нужны [3]. Затем мы вызвали макрос assert! и передали ему результат вызова larger.can_hold(&smaller) [4]. Эта выражение должно возвращать true, поэтому наш тест должен пройти. Проверим!

running 1 test
test tests::larger_can_hold_smaller... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Тест действительно проходит! Добавим еще один тест, на этот раз проверив, что меньшая прямоугольная область не может вместить большую:

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        --snip--
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Поскольку правильный результат работы функции can_hold в этом случае — false, мы должны отрицать этот результат, прежде чем передать его в макрос assert!. Таким образом, наш тест пройдет, если can_hold возвращает false:

running 2 tests
test tests::larger_can_hold_smaller... ok
test tests::smaller_cannot_hold_larger... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Два теста, которые проходят! Теперь посмотрим, что произойдет с нашими результатами тестов, если мы внедрим ошибку в наш код. Мы изменим реализацию метода can_hold, заменив знак больше на знак меньше при сравнении ширины:

--snip--

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

Запуск тестов теперь дает следующий результат:

running 2 tests
test tests::smaller_cannot_hold_larger... ok
test tests::larger_can_hold_smaller... FAILED

failures:

---- tests::larger_can_hold_smaller stdout ----
thread 'main' panicked at 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Наши тесты обнаружили ошибку! Поскольку larger.width равно 8, а smaller.width равно 5, сравнение ширины в методе can_hold теперь возвращает false: 8 не меньше 5.

Тестирование равенства с помощью макросов assert_eq! и assert_ne!

Одним из распространенных способов проверки функциональности является тестирование равенства между результатом проверяемого кода и значением, которое вы ожидаете, что вернет код. Вы могли бы сделать это, используя макрос assert! и передавая ему выражение с использованием оператора ==. Однако это настолько распространенный тест, что стандартная библиотека предоставляет пару макросов — assert_eq! и assert_ne! — для более удобного выполнения этого теста. Эти макросы сравнивают два аргумента на равенство или неравенство соответственно. Они также выведут два значения, если утверждение не пройдет, что делает легче понять, почему тест не прошел; наоборот, макрос assert! только показывает, что он получил значение false для выражения ==, не выводя значения, которые привели к значению false.

В Листинге 11-7 мы пишем функцию под названием add_two, которая добавляет 2 к своему параметру, а затем тестируем эту функцию с использованием макроса assert_eq!.

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

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

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

Листинг 11-7: Тестирование функции add_two с использованием макроса assert_eq!

Проверим, что тест пройдет!

running 1 test
test tests::it_adds_two... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Мы передаем 4 в качестве аргумента в assert_eq!, которое равно результату вызова add_two(2). Строка для этого теста выглядит так: test tests::it_adds_two... ok, и текст ok показывает, что наш тест прошел!

Давайте внесем ошибку в наш код, чтобы увидеть, как выглядит assert_eq!, когда тест не проходит. Изменим реализацию функции add_two так, чтобы она вместо этого добавляла 3:

pub fn add_two(a: i32) -> i32 {
    a + 3
}

Запустим тесты снова:

running 1 test
test tests::it_adds_two... FAILED

failures:

---- tests::it_adds_two stdout ----
1 thread 'main' panicked at 'assertion failed: `(left == right)`
2   left: `4`,
3  right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Наши тесты обнаружили ошибку! Тест it_adds_two не прошел, и сообщение говорит нам, что не пройденное утверждение было assertion failed: (left == right)`\[1\] и какие значения былиleft\[2\] иright\[3\]. Это сообщение помогает нам начать отладку: аргументleftбыл4, а аргумент right, где мы вызывали add_two(2), был 5`. Вы можете представить, насколько это будет полезно, когда у нас много тестов.

Обратите внимание, что в некоторых языках и фреймворках тестирования параметры функций для проверки равенства называются expected и actual, и порядок, в котором мы указываем аргументы, имеет значение. Однако в Rust они называются left и right, и порядок, в котором мы указываем ожидаемое значение и значение, которое генерирует код, не имеет значения. Мы могли бы записать утверждение в этом тесте как assert_eq!(add_two(2), 4), что бы привело к тому же сообщению об ошибке, которое показывает assertion failed: (left == right)``.

Макрос assert_ne! пройдет, если два значения, которые мы передаем ему, не равны, и не пройдет, если они равны. Этот макрос наиболее полезен в случаях, когда мы не знаем, какое значение будет, но мы знаем, что значение не должно быть. Например, если мы тестируем функцию, которая гарантированно изменяет свой ввод каким-то образом, но способ изменения ввода зависит от дня недели, когда мы запускаем наши тесты, лучше всего утверждение может быть, что выход функции не равен входу.

Под капотом макросы assert_eq! и assert_ne! используют операторы == и != соответственно. Когда утверждения не проходят, эти макросы выводят свои аргументы с использованием отладочной форматирования, что означает, что сравниваемые значения должны реализовывать трейты PartialEq и Debug. Все примитивные типы и большинство типов стандартной библиотеки реализуют эти трейты. Для структур и перечислений, которые вы определяете сами, вам нужно реализовать PartialEq, чтобы проверить равенство этих типов. Также вам нужно реализовать Debug, чтобы вывести значения, когда утверждение не проходит. Поскольку оба трейта являются трейтами, которые можно получить автоматически, как упоминалось в Листинге 5-12, это обычно так же просто, как добавить аннотацию #[derive(PartialEq, Debug)] к определению вашей структуры или перечисления. См. Приложение C для более подробной информации о этих и других трейтах, которые можно получить автоматически.

Добавление пользовательских сообщений об ошибке

Вы также можете добавить пользовательское сообщение, которое будет выводиться вместе с сообщением об ошибке, в качестве необязательных аргументов для макросов assert!, assert_eq! и assert_ne!. Любые аргументы, указанные после обязательных аргументов, передаются в макрос format! (обсуждается в разделе "Конкатенация с помощью оператора + или макроса format!"), поэтому вы можете передать строку форматирования, которая содержит плейсхолдеры {} и значения, которые будут подставляться в эти плейсхолдеры. Пользовательские сообщения полезны для документирования того, что означает утверждение; когда тест не проходит, вы будете лучше понимать, в чем проблема с кодом.

Например, предположим, у нас есть функция, которая приветствует людей по имени, и мы хотим проверить, что имя, которое мы передаем в функцию, появляется в выводе:

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

pub fn greeting(name: &str) -> String {
    format!("Hello {name}!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

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

Теперь давайте внесем ошибку в этот код, изменив greeting, чтобы он не включал name, чтобы увидеть, как выглядит стандартное сообщение об ошибке при тестировании:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

Запуск этого теста дает следующий результат:

running 1 test
test tests::greeting_contains_name... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'assertion failed:
result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::greeting_contains_name

Этот результат только показывает, что утверждение не прошло и на какой строке находится утверждение. Более полезное сообщение об ошибке выведет значение из функции greeting. Добавим пользовательское сообщение об ошибке, составленное из строки форматирования с заполненным плейсхолдером фактическим значением, которое мы получили из функции greeting:

#[test]
fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
        result.contains("Carol"),
        "Greeting did not contain name, value was `{result}`"
    );
}

Теперь, когда мы запускаем тест, мы получим более информативное сообщение об ошибке:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value
was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

Мы можем увидеть значение, которое мы фактически получили в выводе теста, что поможет нам отладить, что произошло, вместо того, что мы ожидали.

Проверка на панику с помощью should_panic

Кроме проверки возвращаемых значений, важно проверить, обрабатывает ли наш код ошибочные ситуации, как мы ожидаем. Например, рассмотрим тип Guess, который мы создали в Листинге 9-13. Другие части кода, которые используют Guess, зависят от гарантии, что экземпляры Guess будут содержать только значения от 1 до 100. Мы можем написать тест, который гарантирует, что попытка создать экземпляр Guess с значением за пределами этого диапазона вызовет панику.

Мы это делаем, добавив атрибут should_panic к нашей тестовой функции. Тест пройдет, если код внутри функции вызывает панику; тест не пройдет, если код внутри функции не вызывает панику.

Листинг 11-8 показывает тест, который проверяет, что ошибочные ситуации Guess::new возникают, когда мы ожидаем их.

// src/lib.rs
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Листинг 11-8: Тестирование того, что условие вызовет панику!

Мы помещаем атрибут #[should_panic] после атрибута #[test] и перед тестовой функцией, к которой он относится. Посмотрим на результат, когда этот тест пройдет:

running 1 test
test tests::greater_than_100 - should panic... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

Все хорошо! Теперь давайте внесем ошибку в наш код, удалив условие, при котором функция new будет вызывать панику, если значение больше 100:

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

Когда мы запустим тест из Листинга 11-8, он не пройдет:

running 1 test
test tests::greater_than_100 - should panic... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

В этом случае мы не получаем очень полезного сообщения, но когда мы смотрим на тестовую функцию, мы видим, что она помечена атрибутом #[should_panic]. Неудача, которую мы получили, означает, что код в тестовой функции не вызвал панику.

Тесты, которые используют should_panic, могут быть неточными. Тест с should_panic пройдет даже если тест вызывает панику по другой причине, чем мы ожидали. Чтобы сделать тесты с should_panic более точными, мы можем добавить необязательный параметр expected к атрибуту should_panic. Тестовый механизм убедится, что сообщение об ошибке содержит указанный текст. Например, рассмотрим модифицированный код для Guess в Листинге 11-9, где функция new вызывает панику с разными сообщениями, в зависимости от того, является ли значение слишком малым или слишком большим.

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Листинг 11-9: Тестирование на панику с сообщением о панике, содержащим указанную подстроку

Этот тест пройдет, потому что значение, которое мы указали в параметре expected атрибута should_panic, является подстрокой сообщения, с которым функция Guess::new вызывает панику. Мы могли бы указать целое сообщение о панике, которое мы ожидаем, в этом случае оно было бы Guess value must be less than or equal to 100, got 200. То, что вы выбираете указывать, зависит от того, насколько уникально или динамично сообщение о панике и насколько точно вы хотите, чтобы был ваш тест. В этом случае подстрока сообщения о панике достаточно, чтобы убедиться, что код в тестовой функции выполняет блок else if value > 100.

Посмотрим, что произойдет, когда тест с should_panic и ожидаемым сообщением не пройдет. Давайте снова внесем ошибку в наш код, поменяв тела блоков if value < 1 и else if value > 100:

// src/lib.rs
--snip--
if value < 1 {
    panic!(
        "Guess value must be less than or equal to 100, got {}.",
        value
    );
} else if value > 100 {
    panic!(
        "Guess value must be greater than or equal to 1, got {}.",
        value
    );
}
--snip--

На этот раз, когда мы запустим тест с should_panic, он не пройдет:

running 1 test
test tests::greater_than_100 - should panic... FAILED

failures:

---- tests::greater_than_100 stdout ----
thread 'main' panicked at 'Guess value must be greater than or equal to 1, got
200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: `"Guess value must be greater than or equal to 1, got
200."`,
 expected substring: `"less than or equal to 100"`

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s

Сообщение об ошибке показывает, что этот тест действительно вызвал панику, как мы ожидали, но сообщение о панике не содержало ожидаемой строки 'Guess value must be less than or equal to 100'. Сообщение о панике, которое мы получили в этом случае, было Guess value must be greater than or equal to 1, got 200. Теперь мы можем начать определять, где находится наша ошибка!

Использование Result<T, E> в тестах

Наши тесты до сих пор все вызывают панику, когда они не проходят. Мы также можем писать тесты, которые используют Result<T, E>! Вот тест из Листинга 11-1, переписанный для использования Result<T, E> и возврата Err вместо вызова паники:

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

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

Функция it_works теперь имеет тип возврата Result<(), String>. В теле функции вместо вызова макроса assert_eq! мы возвращаем Ok(()), когда тест проходит, и Err с String внутри, когда тест не проходит.

Писание тестов так, чтобы они возвращали Result<T, E>, позволяет использовать оператор вопроса в теле тестов, что может быть удобным способом написания тестов, которые должны не пройти, если любая операция внутри них возвращает вариант Err.

Вы не можете использовать аннотацию #[should_panic] для тестов, которые используют Result<T, E>. Чтобы проверить, что операция возвращает вариант Err, не используйте оператор вопроса для значения Result<T, E>. Вместо этого используйте assert!(value.is_err()).

Теперь, когда вы знаете несколько способов писать тесты, давайте посмотрим, что происходит, когда мы запускаем наши тесты, и исследуем разные параметры, которые мы можем использовать с cargo test.

Резюме

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