How do I write a Rust unit test that ensures that a panic has occurred?

我有一个 Rust 函数,在某些条件下,我希望编写一个测试用例来验证函数是否恐慌。除了 assert!assert_eq!宏,我什么都找不到。有什么机制可以测试这个吗?

我可以生成一个新任务,并检查该任务是否惊慌失措。这样做有意义吗?


返回一个 Result<T, E>不适合我的情况。

我希望添加对 Add特性的支持到我正在实现的 Matrix类型。这种添加的理想语法如下:

let m = m1 + m2 + m3;

其中 m1m2m3都是矩阵。因此,add的结果类型应该是 Matrix。下面这些内容太隐晦了:

let m = ((m1 + m2).unwrap() + m3).unwrap()

同时,add()函数需要验证添加的两个矩阵是否具有相同的维度。因此,如果尺寸不匹配,add()需要恐慌。可用的选项是 panic!()

36477 次浏览

您可以在 Rust book 的 测试部分找到答案。更具体地说,您想要 #[should_panic]属性:

#[test]
#[should_panic]
fn test_invalid_matrices_multiplication() {
let m1 = Matrix::new(3, 4);  // assume these are dimensions
let m2 = Matrix::new(5, 6);
m1 * m2
}

如果您想断言测试函数只有一个特定的部分失败,那么使用 std::panic::catch_unwind()并检查它是否返回一个 Err,例如使用 is_err()。在复杂的测试函数中,这有助于确保测试不会因为早期失败而错误地通过。

Rust 标准库中的几个 测试使用这种技术。

As Francis Gagné mentioned in his answer, I also find the #[should_panic] attribute to not be fine-grained enough for more complex tests--for example, if my test setup fails for some reason (i.e. I've written a bad test), I want a panic to be considered a failure!

从 Rust 1.9.0开始,std::panic::catch_unwind()就可用了。它允许您将预期会产生恐慌的代码放入闭包中,并且只有 那个代码发出的恐慌才会被认为是预期的(即通过测试)。

#[test]
fn test_something() {
... //<-- Any panics here will cause test failure (good)
let result = std::panic::catch_unwind(|| <expected_to_panic_operation_here>);
assert!(result.is_err());  //probe further for specific error type here, if desired
}

请注意,它不能捕捉非平仓恐慌(例如 std::process::abort())。

作为附录:@U007D 提出的解决方案也适用于 doctest:

/// My identity function that panic for an input of 42.
///
/// ```
/// assert_eq!(my_crate::my_func(23), 23);
///
/// let result = std::panic::catch_unwind(|| my_crate::my_func(42));
/// assert!(result.is_err());
/// ```
pub fn my_func(input: u32) -> u32 {
if input == 42 {
panic!("Error message.");
} else {
input
}
}

Use following catch_unwind_silent instead of regular catch_unwind to achieve silence in output for expected exceptions:

use std::panic;


fn catch_unwind_silent<F: FnOnce() -> R + panic::UnwindSafe, R>(f: F) -> std::thread::Result<R> {
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let result = panic::catch_unwind(f);
panic::set_hook(prev_hook);
result
}

使用 #[should_panic]属性的可接受答案的主要问题是:

  • 不相关的恐慌可能导致测试通过
  • 它不会禁止向控制台打印紧急消息,从而导致测试执行日志不清晰
  • 在恐慌发生后不可能再增加额外的检查

作为一个更好的选择,我强烈推荐 查看名为 < a href = “ https://github.com/mirind4/fluent-asserter”rel = “ nofollow noReferrer”> fluent-asserter 的库

通过使用它,您可以很容易地编写一个断言来检查是否发生了恐慌,如下所示:

#[test]
fn assert_that_code_panics() {
let panicking_action = || panic!("some panic message");


assert_that_code!(panicking_action)
.panics()
.with_message("some panic message");
}

这样做的好处是:

  • 它使用一个流畅的接口,导致一个可读的断言
  • 它禁止将紧急消息打印到控制台,从而产生一个干净的测试执行日志
  • 您可以在紧急检查之后添加其他断言

当使用生锈箱 test_case时,使用 panics习语。

extern crate test_case;
use test_case::test_case;


#[test_case(0 => panics)]
#[test_case(1)]
fn test_divisor(divisor: usize) {
let _result = 1 / divisor;
}

来自 测试恐慌部分的单元测试文档

pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
if b == 0 {
panic!("Divide-by-zero error");
} else if a < b {
panic!("Divide result is zero");
}
a / b
}


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


#[test]
fn test_divide() {
assert_eq!(divide_non_zero_result(10, 2), 5);
}


#[test]
#[should_panic]
fn test_any_panic() {
divide_non_zero_result(1, 0);
}


#[test]
#[should_panic(expected = "Divide result is zero")]
fn test_specific_panic() {
divide_non_zero_result(1, 10);
}
}

运行 cargo test时的输出为

$ cargo test


running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok


failures:


---- tests::test_bad_add stdout ----
thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
left: `-1`,
right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.




failures:
tests::test_bad_add


test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out