Тестирование в Rust
Тестирование встроено прямо в Rust и cargo, так что для начала не нужна никакая отдельная библиотека. На этой странице ровно столько, сколько нужно, чтобы написать и запустить первые тесты.
Пишем простой тест
Заголовок раздела «Пишем простой тест»Тест — это обычная функция с пометкой #[test]:
fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }}На что стоит обратить внимание:
#[cfg(test)]говорит Rust, что этот модуль компилируется только при запуске тестов. В обычную программу он не входит.use super::*;подключает всё из родительского модуля (здесь — функциюadd), чтобы тест мог её вызвать.#[test]помечает конкретную функцию как тест, который должен запуститьcargo test.
Запускаем тесты
Заголовок раздела «Запускаем тесты»Из папки проекта:
cargo testТы увидишь примерно такой вывод:
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredCargo запустил один тест, он прошёл (ok): 1 пройден, 0 провалено, 0 пропущено.
assert! и его друзья
Заголовок раздела «assert! и его друзья»Три макроса покрывают почти всё, что тебе понадобится:
#[test]fn checks() { assert!(2 + 2 == 4); // тест падает, если условие ложно assert_eq!(2 + 2, 4); // падает, если два значения не равны assert_ne!(2 + 2, 5); // падает, если два значения РАВНЫ}Обычно assert_eq! и assert_ne! лучше, чем простой assert!(a == b): когда они падают, то выводят оба значения. Ты сразу видишь, что не совпало, а не просто «assertion failed».
Упавший тест и как он выглядит
Заголовок раздела «Упавший тест и как он выглядит»fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn this_will_fail() { assert_eq!(add(2, 2), 5); }}test tests::this_will_fail ... FAILED
assertion `left == right` failed left: 4 right: 5Именно такое сообщение и хочется видеть, когда тест падает: слева (left) — что на самом деле выдал твой код, справа (right) — что ожидал тест.
Проверяем, что функция паникует
Заголовок раздела «Проверяем, что функция паникует»Иногда правильное поведение — это и есть паника. Например, функция должна отказываться от неверных данных. Для этого есть #[should_panic]:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("нельзя делить на ноль"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}Понятные сообщения об ошибках
Заголовок раздела «Понятные сообщения об ошибках»Когда тест падает, хочется как можно быстрее понять почему. Здесь очень помогают два маленьких дополнения.
Своё сообщение. assert!, assert_eq! и assert_ne! принимают после значений дополнительное сообщение, которое пишется так же, как в println!. Оно выводится, только если проверка не прошла.
expected в should_panic. Простой #[should_panic] проходит при любой панике, даже если её вызвала совсем другая ошибка. Если добавить expected = "...", тест пройдёт, только когда сообщение паники содержит этот текст.
Вот снова функция divide, теперь с main, чтобы её можно было запустить, и два теста, которые используют обе идеи:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("нельзя делить на ноль"); } a / b}
fn main() { println!("10 / 2 = {}", divide(10, 2)); println!("7 / 2 = {}", divide(7, 2));}
#[cfg(test)]mod tests { use super::*;
#[test] fn rounds_down() { let result = divide(7, 2); assert_eq!(result, 3, "7 / 2 должно округляться вниз, а получилось {result}"); }
#[test] #[should_panic(expected = "делить на ноль")] fn dividing_by_zero_panics() { divide(10, 0); }}Вывод:
10 / 2 = 57 / 2 = 3Вывод выше — это то, что печатает cargo run, потому что main тесты не запускает. Чтобы запустить тесты, используй cargo test. В Rust Playground открой меню рядом с кнопкой Run и выбери Test. Ты увидишь:
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outТесты выполняются параллельно, поэтому две строки test ... могут появиться в любом порядке. А теперь представь, что тест ожидал 4, а не 3. В сообщении о провале будет твой текст, прямо рядом с двумя значениями:
assertion `left == right` failed: 7 / 2 должно округляться вниз, а получилось 3 left: 3 right: 4А если divide запаникует с другим сообщением, проверка expected это поймает. Здесь тест ждал слово "negative", а в сообщении паники его нет:
note: panic did not contain expected string panic message: "нельзя делить на ноль" expected substring: "negative"Зачем вообще писать тесты
Заголовок раздела «Зачем вообще писать тесты»Честный ответ: для короткого скрипта ты, скорее всего, не станешь. Тесты начинают окупаться, когда код будут использовать повторно или менять позже. С ними можно смело переделывать код (рефакторинг): cargo test сразу скажет, если ты что-то сломал, и тебе не придётся узнавать об этом от пользователя.
Тесты могут жить и в документации: примеры кода внутри документирующих комментариев /// тоже запускаются через cargo test, так что твои примеры никогда не устареют. Как это сделать, показано в уроке Документация.
Практика
Заголовок раздела «Практика»В обоих упражнениях у решения есть main, которую можно запустить, и тесты, которые запускаются через cargo test (или Test в Playground).
1. Протестируй функцию выставления оценок
Заголовок раздела «1. Протестируй функцию выставления оценок»Напиши fn grade(score: u32) -> char, которая возвращает 'A' за 90 баллов и выше, 'B' за 80–89, 'C' за 70–79 и 'F' за всё, что ниже. Затем напиши тесты, которые проверяют границы, например 90 и 89. Подсказка: ошибки прячутся именно на границах. Тест ровно для 90 ловит классическую ошибку — > вместо >=.
Показать решение
fn grade(score: u32) -> char { if score >= 90 { 'A' } else if score >= 80 { 'B' } else if score >= 70 { 'C' } else { 'F' }}
fn main() { for score in [95, 85, 72, 40] { println!("{score} -> {}", grade(score)); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn edges_of_a() { assert_eq!(grade(90), 'A'); assert_eq!(grade(89), 'B'); }
#[test] fn edges_of_c() { assert_eq!(grade(70), 'C'); assert_eq!(grade(69), 'F'); }
#[test] fn zero_is_f() { assert_eq!(grade(0), 'F', "за 0 баллов должна быть оценка F"); }}Вывод:
95 -> A85 -> B72 -> C40 -> Fcargo test должен сообщить 3 passed. Попробуй заменить score >= 90 на score > 90 и снова запустить тесты: edges_of_a упадёт и покажет left: 'B' и right: 'A'.
2. Протестируй функцию, которая возвращает Option
Заголовок раздела «2. Протестируй функцию, которая возвращает Option»Напиши fn safe_divide(a: i32, b: i32) -> Option<i32>, которая при b, равном 0, возвращает None вместо паники. Напиши один тест для обычного деления и один для деления на ноль. Подсказка: assert_eq! работает и с Option, так что можно написать assert_eq!(safe_divide(1, 0), None).
Показать решение
fn safe_divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None } else { Some(a / b) }}
fn main() { println!("{:?}", safe_divide(10, 2)); println!("{:?}", safe_divide(1, 0));}
#[cfg(test)]mod tests { use super::*;
#[test] fn divides_normally() { assert_eq!(safe_divide(10, 2), Some(5)); }
#[test] fn zero_gives_none() { assert_eq!(safe_divide(1, 0), None); }}Вывод:
Some(5)NoneСравни это с функцией divide выше. Там деление на ноль — это паника, поэтому тесту нужен #[should_panic]. Здесь это просто обычное возвращаемое значение, и хватает простого assert_eq!. Функции, которые возвращают Option или Result, обычно проще тестировать.
- Пометь функцию
#[test], чтобы она стала тестом. Все тесты запускаются черезcargo test. assert!,assert_eq!иassert_ne!покрывают большинство повседневных проверок. Выбирайassert_eq!/assert_ne!— у них понятнее сообщения о провале.#[should_panic]проверяет, что функция паникует, когда должна. Добавьexpected = "...", чтобы убедиться, что она запаниковала по правильной причине.- Добавь своё сообщение в любой макрос assert, чтобы при провале было понятно, что пошло не так.
- Больше всего пользы от тестов для кода, который ты собираешься менять или использовать повторно.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Какой атрибут помечает функцию как тест, который должен запустить cargo test?
2. Почему assert_eq! и assert_ne! обычно лучше простого assert!(a == b)?
3. Что делает #[should_panic]?
Результат: 0 / 3
Что почитать
Заголовок раздела «Что почитать»- Как писать тесты — книга «Язык программирования Rust» (перевод на русский):
#[test], макросы assert, свои сообщения,should_panicи тесты, которые возвращаютResult. - Управление выполнением тестов — книга «Язык программирования Rust» (перевод на русский): как запустить один тест по имени, показать вывод
println!и использовать#[ignore]. - Организация тестов — книга «Язык программирования Rust» (перевод на русский): чем модульные тесты отличаются от интеграционных в папке
tests/. - Unit-тестирование — Rust на примерах (перевод на русский): короткие примеры тестов, которые проходят, падают и паникуют.
cargo test— The Cargo Book (на английском): все параметры командыcargo test.