Як писати тести

Тести - це функції Rust, які перевіряють, чи тестований код працює у очікуваний спосіб. Тіла тестових функцій зазвичай виконують наступні три дії:

  1. Встановити будь-яке потрібне значення або стан.
  2. Запустити на виконання код, який ви хочете протестувати.
  3. Переконатися, що отримані результати відповідають вашим очікуванням.

Розгляньмо функціонал, наданий 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 {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

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

Наразі проігноруймо два верхні рядки та зосередимося на функції. Зверніть увагу на анотацію #[test]: цей атрибут вказує на те, що функція є тестувальною, тож функціонал для запуску тестів ставитиметься до неї, як до тесту. Ми також можемо мати нетестувальні функції в модулі tests, щоб допомогти налаштувати типові сценарії або виконати загальні операції, тому ми завжди повинні позначати анотаціями, які саме функції є тестувальними.

Тіло функції зі приклада використовує макрос assert_eq! для ствердження того, що 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)

running 1 test
test tests::it_works ... 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

Блок коду 11-2: виведення після запуску автоматично згенерованого тесту

Cargo скомпілював та запустив тест. Ми бачимо рядок running 1 test. Наступний рядок показує назву згенерованої тестувальної функції it_works, а також що результат запуску тесту є ok. Загальний результат test result: ok. означає, що усі тести пройшли, а частина 1 passed; 0 failed показує загальну кількість тестів що були пройдені та провалилися.

Можна позначити деякі тести як ігноровані, тоді вони не будуть запускатися; ми розглянемо це далі у цьому розділі у підрозділі "Ігнорування окремих тестів без спеціального уточнення" . Оскільки ми не позначали жодного тесту для ігнорування, то отримуємо на виході 0 ignored. Ми також можемо додати до команди cargo test аргумент, щоб запускалися лише ті тести, що відповідають певній стрічці; Це називається фільтрацією та ми поговоримо про це у підрозділі "Запуск підмножини тестів за назвою" . Ми також не маємо відфільтрованих тестів, тому вивід показує 0 filtered out.

0 measured показує статистику бенчмарків, що тестують швидкодію. Бенчмарки на момент написання цієї статті доступні лише у нічних збірках Rust. Дивись документацію про бенчмарки для більш детальної інформації.

Наступна частина виводу Doc-tests adder призначена для результатів документаційних тестів. У нас поки що немає документаційних тестів, але Rust може скомпілювати будь-які приклади коду з документації по нашому API. Ця функція допомагає синхронізувати вашу документацію та код! Ми розглянемо, як писати документаційні тести в підрозділі “Документаційні коментарі як тести” Розділу 14. Зараз ми проігноруємо частину виводу, присвячену Doc-tests.

Налаштуймо тест для відповідності нашим потребам. Спочатку змінимо назву тестової функції it_works на іншу, наприклад exploration, ось так:

Файл: src/lib.rs

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

Далі знову запустимо cargo test. Вивід тепер покаже exploration замість it_works:

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

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

   Doc-tests adder

running 0 tests

test result: ok. 0 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 провалився.

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

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

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


failures:
    tests::another

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. Дві нові секції з'явилися між результатами окремих тестів та загальними результатами: перша показує детальну причину того, чому тест провалився. У цьому випадку ми отримали те, що тест another провалився тому, що panicked at 'Make this test fail' у рядку 10 у файлі src/lib.rs. У наступній секції наведені назви тестів, що провалилися, і це зручно коли у нас багато таких тестів та багато деталей про провали. Ми можемо використати назву тесту для його подальшого зневадження; ми поговоримо більше про запуск тестів у підрозділі Керування запуском тестів" .

У кінці показується підсумковий результат тестування: в цілому результат нашого тесту FAILED. У нас один тест пройшов, та один провалився.

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

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

Макрос assert!, що надається стандартною бібліотекою, широко використовується для того, щоб впевнитися, що деяка умова у тесті приймає значення true. Ми даємо макросу assert! аргумент, який обчислюється як вираз булевого типу. Якщо значення обчислюється як true, нічого поганого не трапляється та тест вважається пройденим. Якщо ж значення буде false макрос assert! викликає паніку panic!, що спричиняє провал тесту. Використання макросу assert! допомагає нам перевірити, чи працює наш код в очікуваний спосіб.

У Розділі 5, Блок коду 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

#[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
    }
}

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

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

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

Блок коду 11-6: тест для can_hold, що перевіряє, чи більший прямокутник справді може вмістити менший

Зверніть увагу, що ми додали новий рядок всередині модуля tests: use super::*;. Модуль tests є звичайним модулем, що слідує звичайним правилам видимості, про які ми розповідали у підрозділі “Шляхи для посилання на елемент в дереві модулів” Розділу 7. Оскільки модуль tests є внутрішнім модулем, нам потрібно ввести код для тестування з зовнішнього модуля до області видимості внутрішнього модуля. Ми використовуємо глобальний режим для того, щоб все, що визначене у зовнішньому модулі, було доступним к модулі tests.

Ми назвали наш тест larger_can_hold_smallerі створили потрібні нам два екземпляри Rectangle. Тоді ми викликали макрос assert! і передали йому результат виклику larger.can_hold(&smaller). Цей вираз повинен повернути true, тому наш тест повинен пройти. З'ясуймо, чи це так!

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

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

   Doc-tests rectangle

running 0 tests

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

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

Файл: 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
    }
}

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

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

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

    #[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:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

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

   Doc-tests rectangle

running 0 tests

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

Проходять вже два тести! Тепер подивімося, що відбувається з результатами тесту, якщо в код додати ваду. Ми змінимо реалізацію методу can_hold, замінивши знак більше на менше при порівняння ширин:

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

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

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

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

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

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

Запуск тестів тепер виводить таке:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

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

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

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

Наші тести викрили ваду! Оскільки 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!

Переконаймося, що вона проходить!

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

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

   Doc-tests adder

running 0 tests

test result: ok. 0 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
}

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

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

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

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

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

failures:

---- tests::it_adds_two stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `4`,
 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

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

Наш тест виявив ваду! Тест it_adds_two провалився, і повідомлення каже нам про те, що провалене ствердження було assertion failed: `(left == right)` і значення left і right. Це повідомлення допомагає нам почати зневадження: аргумент 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! використовують оператори == and !=, відповідно. Коли ствердження провалюється, ці макроси друкують свої аргументи з використанням форматування налагодження, тобто, що значення, які порівнюються, повинні реалізовувати трейти PartialEq і Debug. Всі примітивні типи та більшість типів зі стандартної бібліотеки реалізовують ці трейти. Для структур та енумів, які ви визначаєте самостійно, вам потрібно буде реалізувати PartialEq, щоб стверджувати рівність таких типів. Також потрібно буде реалізувати Debug для виведення значень, коли ствердження провалюється. Оскільки обидва ці трейти вивідні, як було зазначено в Блоці коду 5-12 у Розділі 5, зазвичай просто треба додати анотацію #[derive(PartialEq, Debug) до визначення вашої структури чи енуму. Дивіться Додаток C, "Вивідні трейти", для детальнішої інформації про ці та інші вивідні трейти.

Додавання користувацьких повідомлень про провали

Ви також можете додати користувальницьке повідомлення для виведення з повідомленням про помилку, як додатковий аргументи до макросів assert!, assert_eq!, і assert_ne!. Будь-які аргументи, вказані після необхідних аргументів, передаються до макпрсу format! (обговорюється у Розділі 8 у підрозділі "Конкатенація оператором + або макросом 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 s виключивши name, щоб побачити, як виглядає тест провалу за замовчуванням:

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

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

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

Запуск цього тесту виводить таке:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

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

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

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

Цей результат вказує на те, що ствердження провалилося і в якому рядку. Більш корисне повідомлення про помилку може вивести значення з функції greeting. Додаймо власне повідомлення про помилку, складене зі стрічки форматування з заповнювачем, заповненим фактичним значенням, яке ми отримали від функції greeting:

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

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

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

Тепер, коли ми запустимо тест, ми отримаємо більш інформативне повідомлення про помилку:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

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

failures:

---- 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


failures:
    tests::greeting_contains_name

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

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

Ми можемо бачити значення, яке ми фактично отримали на виході тесту, що допоможе нам налагодити, що сталося замість того, що ми очікували.

Перевірка на паніку за допомогою should_panic

На додаток до перевірки значень, повернених з функцій, важливо перевірити, чи наш код обробляє помилкові стани, які ми очікуємо. Наприклад, розгляньте тип Guess, який ми створили у Блоці коду 9-13 у Розділі 9. Інший код, який використовує 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: Тестування умови, що призводить до panic!

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

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

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

   Doc-tests guessing_game

running 0 tests

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

Здається, усе гаразд! Тепер додамо ваду у наш код, видаливши умову, що функція new запанікує, якщо значення більше за 100:

pub struct Guess {
    value: i32,
}

// --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 }
    }
}

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

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

Коли ми запустимо тест з Блоку коду 11-8, він провалюється:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

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

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

В цьому випадку ми отримуємо не дуже помічне повідомлення, але коли подивитися на тестову функцію, то бачимо, що вона анотована #[should_panic]. Отриманий провал означає, що код у тестовій функції не призвів до паніки.

Тести, що використовують should_panic, можуть бути неточними. Тест із should_panic пройде, навіть якщо тест запанікує з іншої причини, а не очікуваної. Щоб зробити тести із should_panic більш точними, ми можемо додати необов'язковий параметр expected до атрибута should_panic. Тестова оболонка забезпечить, щоб повідомлення про провал містило наданий текст. Наприклад, розгляньте модифікований код Guess у Блоці коду 11-9, де функція new панікує з різними повідомленнями залежно від того, значення занадто мале або занадто велике.

Файл: src/lib.rs

pub struct Guess {
    value: i32,
}

// --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: тестування panic! з повідомленням паніки, що містить зазначену підстрічку

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

Щоб побачити, що трапиться, якщо тест should_panic з повідомленням expected провалюється, знову додамо ваду у наш код, обмінявши тіла блоків if value < 1 та else if value > 100:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        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
            );
        }

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

Цього разу, коли ми запускаємо тест should_panic, він провалиться:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

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

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

Повідомлення про провал вказує на те, що цей тест дійсно панікував, як ми очікували, але повідомлення про паніку не містить очікувану стрічку '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 замість паніки:

#[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. ch08-02-strings.html#concatenation-with-the--operator-or-the-format-macro ch11-02-running-tests.html#controlling-how-tests-are-run