Avatar ·

Как написать юнит-тест в Rust, проверяющий, что функция паникует?

Всем привет! Я только начал осваивать Rust и пытаюсь написать тесты для своей библиотеки. У меня есть функция, которая при определённых условиях должна вызывать панику. Например, я написал простой калькулятор, и функция деления должна паниковать при попытке деления на ноль. Вот мой код: ```rust fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("Division by zero!"); } a / b } ``` Я хочу написать юнит-тест, который проверяет, что при вызове `divide(10, 0)` действительно происходит паника. Я пробовал использовать `#[should_panic]`, но не уверен, что делаю это правильно. Кроме того, я слышал, что есть более продвинутые способы проверки паники с сообщением. Подскажите, как правильно писать такие тесты в Rust? Может быть, есть какие-то best practices или альтернативные подходы? Заранее спасибо за помощь! <<>>

Основной способ: атрибут #[should_panic]

Самый простой и распространённый способ проверить, что функция вызывает панику — использовать атрибут #[should_panic]. Он указывает тестовому фреймворку, что тест считается успешным только в том случае, если внутри него произошла паника.

Вот как это выглядит для вашего примера:

#[test]
#[should_panic]
fn test_divide_by_zero() {
    divide(10, 0);
}

Если функция divide вызовет панику — тест пройдёт. Если нет — тест упадёт с сообщением о том, что ожидалась паника, но её не произошло.

Проверка сообщения паники

Часто бывает полезно убедиться не просто в факте паники, но и в том, что сообщение паники содержит определённый текст. Для этого используется #[should_panic(expected = "...")]:

#[test]
#[should_panic(expected = "Division by zero!")]
fn test_divide_by_zero_with_message() {
    divide(10, 0);
}

Теперь тест пройдёт только если паника произошла и её сообщение содержит подстроку "Division by zero!". Это помогает избежать ложных срабатываний, когда паника происходит по другой причине.

Альтернатива: std::panic::catch_unwind

Иногда нужно сделать более сложную проверку — например, проверить несколько сценариев в одном тесте или выполнить дополнительную логику после паники. Для этого можно использовать catch_unwind:

use std::panic;

#[test]
fn test_divide_by_zero_catch() {
    let result = panic::catch_unwind(|| {
        divide(10, 0);
    });
    assert!(result.is_err()); // Проверяем, что паника произошла
}

Этот метод возвращает Result: Ok(()) если паники не было, и Err(Box<dyn Any>) если была. Вы даже можете извлечь сообщение паники:

#[test]
fn test_divide_by_zero_catch_with_message() {
    let result = panic::catch_unwind(|| {
        divide(10, 0);
    });
    let err = result.unwrap_err();
    let msg = err.downcast_ref::<String>().unwrap();
    assert_eq!(msg, "Division by zero!");
}

Best practices

  • Используйте #[should_panic] для простых случаев — это самый лаконичный и читаемый способ.
  • Указывайте expected — это делает тесты более точными и документирует ожидаемое поведение.
  • Не злоупотребляйте паникой — в Rust паника обычно означает неисправимую ошибку. Для ожидаемых ошибок лучше использовать Result<T, E>.
  • Используйте catch_unwind только когда нужно — например, для тестирования сложной логики или при работе с асинхронным кодом.

Надеюсь, это поможет! Удачи с изучением Rust!

<<>>

Дополнительный подход: тестирование через Result

Иногда вместо паники лучше использовать возврат Result. Это делает код более идиоматичным для Rust и упрощает тестирование. Например, можно переписать вашу функцию так:

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("Division by zero!".to_string());
    }
    Ok(a / b)
}

#[test]
fn test_divide_by_zero_result() {
    let result = divide(10, 0);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "Division by zero!");
}

Этот подход более гибкий: вы можете обрабатывать ошибки без паники, а тесты становятся чище и не требуют специальных атрибутов. Однако если вы работаете с чужой библиотекой, которая паникует, то #[should_panic] или catch_unwind остаются лучшим выбором.

Ещё один совет: в тестах можно комбинировать оба подхода. Например, сначала протестировать нормальное поведение, а потом — панику в отдельном тесте. Это улучшает читаемость и поддержку кода.