我有一个Rust函数,它在某种条件下是panic
,我想写一个测试用例来验证该函数是否正在恐慌。除了assert!
和assert_eq!
宏,我什么都找不到。有什么机制可以测试这一点吗?
我可以生成一个新任务,并检查该任务是否恐慌。这有道理吗?
在我的情况下,返回Result<T, E>
是不合适的。
我希望将对Add
特性的支持添加到我正在实现的Matrix
类型中。这种添加的理想语法如下:
let m = m1 + m2 + m3;
其中m1
、m2
、m3
都是矩阵。因此,add
的结果类型应该是Matrix
。下面这样的东西太神秘了:
let m = ((m1 + m2).unwrap() + m3).unwrap()
同时,add()
函数需要验证被添加的两个矩阵具有相同的维度。因此,如果尺寸不匹配,add()
需要恐慌。可用选项为panic!()
。
您可以在Rust书的测试部分找到答案。更具体地说,您想要#[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
}
正如Francis Gagné在他的回答中提到的,我还发现#[should_panic]
属性(正如公认的答案所建议的)不够细粒度。例如,如果我的测试设置由于某种原因失败(即我写了一个糟糕的测试),我确实希望恐慌被视为失败!
自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()
)。
如果您想断言只有测试函数的特定部分失败,请使用std::panic::catch_unwind()
并检查它是否返回Err
,例如使用is_err()
。在复杂的测试函数中,这有助于确保测试不会因为早期失败而错误通过。
Rust标准库中的一些测试本身就使用了这种技术。
使用以下catch_unwind_silent
而不是常规catch_unwind
来实现预期异常的输出静音:
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
}
作为附录:@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
}
}
来自单元测试文档,在测试恐慌部分
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
使用#[should_panic]
属性接受答案的主要问题是:
- 无关的恐慌可能会导致测试通过
- 它不会抑制将死机消息打印到控制台,从而导致测试执行日志不干净
- 恐慌发生后,无法添加额外的检查
作为更好的选择,我强烈建议查看名为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;
}