我该如何编写一个Rust单元测试来确保发生了恐慌



我有一个Rust函数,它在某种条件下是panic,我想写一个测试用例来验证该函数是否正在恐慌。除了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!()

您可以在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;
}

最新更新