我该如何编写一个 Rust 单元测试以确保已发生 panic?

149
我有一个Rust函数,在某些条件下会panic,我希望编写一个测试用例来验证该函数是否会发生panic。除了assert!assert_eq!宏之外,我找不到其他任何东西。是否有一些机制可以进行测试?
我可以生成一个新任务并检查该任务是否会发生panic。这有意义吗?
在我的情况下,返回Result<T,E>不适用。
我希望为我正在实现的Matrix类型添加对Add特性的支持。这种加法的理想语法如下:
let m = m1 + m2 + m3;

m1m2m3都是矩阵。因此,add的结果类型应该是Matrix。以下这种描述太含糊了:

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

同时,add()函数需要验证要相加的两个矩阵是否具有相同的维度。因此,如果维度不匹配,add()需要引发panic异常。可用的选项是panic!()
8个回答

218

你可以在Rust书的测试章节中找到答案,具体来说,你需要使用#[should_panic]属性:testing

#[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
}

79
值得一提的是,你可以添加一个检查 panic 文本的语句:#[should_panic(expected = "assertion failed")] - phss
8
需要翻译的内容:这里需要说明一下,根据你使用的IDE和环境,恐慌中的堆栈跟踪仍然可能出现在输出中,但测试仍将通过。我用了一分钟才意识到我的"#[should_panic]"实际上是起作用了的。当你从命令行运行通用的"cargo test"时,你会注意到它吞掉了恐慌,并且只显示为"ok"。 - ragona
8
我会把“can”加强为“真的应该”。代码可能有很多种出错情况。如果你不指定预期文本,测试可能会因错误的原因通过。 - Lambda Fairy

86
正如Francis Gagné在他的回答中提到的那样,我也发现#[should_panic]属性(正如接受的答案所建议的)不够细致。例如,如果我的测试设置因某种原因失败(即我编写了一个错误的测试),我确实希望将panic视为失败!
从Rust 1.9.0开始,std::panic::catch_unwind()可用。它允许您将预期会panic的代码放入闭包中,只有由该代码引发的panic才会被视为预期的(即通过的测试)。
#[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
}

注意,它无法捕获非正常终止的 panic(例如 std::process::abort())。

29

7
针对这个问题,应该添加一个 assert_failsassert_panics 宏来处理吗? - dhardy
3
你也可以使用unwrap_err - wimh
有没有办法在#[no_std]环境下实现这个功能?我想创建一个可供常规使用的assert_panics!宏。 - jhpratt
@jhpratt 目前我发现最适合 #[no_std] 的方法是使用 #[should_panic(expected = "<panic message to match>")],并在应该 panic 的测试条件后面加上 unreachable!()。如果预期的 panic 发生,则测试为绿色,否则为红色。 - U007D

13

使用以下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
}

5
作为补充说明:@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
    }
}

2

从 Rust 单元测试文档的 Testing Panics 部分可知:

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

1
使用#[should_panic]属性的主要问题如下:
  • 与panic无关的问题可能会导致测试通过
  • 它不会抑制打印到控制台的panic消息,造成不干净的测试执行日志
  • 在panic发生后无法添加其他检查
作为更好的选择,我强烈推荐检查名为fluent-asserter的库。
通过使用它,您可以轻松编写一个断言来检查是否发生了panic,如下所示:
#[test]
fn assert_that_code_panics() {
    let panicking_action = || panic!("some panic message");

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

这种方式的好处是:
  • 它使用流畅接口,使断言更易读
  • 它抑制了将panic消息打印到控制台,从而使测试执行日志更加清晰
  • 您可以在panic检查后添加其他断言

0

在使用 Rust 包 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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接