Shailesh Kumar
Shailesh Kumar

Reputation: 6977

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

I have a Rust function that panics under some condition and I wish to write a test case to validate whether the function is panicking or not. I couldn't find anything except the assert! and assert_eq! macros. Is there some mechanism for testing this?

I could spawn a new task and checking whether that task panics or not. Does it make sense?


Returning a Result<T, E> is not suitable in my case.

I wish to add support for the Add trait to a Matrix type I am implementing. The ideal syntax for such addition would look like:

let m = m1 + m2 + m3;

where m1, m2, m3 are all matrices. Hence, the result type of add should be Matrix. Something like the following would be too cryptic:

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

At the same time, the add() function needs to validate that the two matrices being added have same dimension. Thus, add() needs to panic if the dimensions don't match. The available option is panic!().

Upvotes: 180

Views: 66947

Answers (8)

U007D
U007D

Reputation: 6348

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

As of Rust 1.9.0, std::panic::catch_unwind() is available. It allows you to put the code you expect to panic into a closure, and only panics emitted by that code will be considered expected (i.e. a passing test).

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

Note it cannot catch non-unwinding panics (e.g. std::process::abort()).

Upvotes: 98

Leo Borai
Leo Borai

Reputation: 2509

From the Documentation on Unit testing, in the Testing Panics section

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);
    }
}

The output when running cargo test would be

$ 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

Upvotes: 3

JamesThomasMoon
JamesThomasMoon

Reputation: 7164

When using rust crate test_case, use the panics idiom.

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;
}

Upvotes: 0

mirind4
mirind4

Reputation: 1573

The main issues with the accepted answer with using #[should_panic] attributes are:

  • unrelated panics might cause a test to pass
  • it does not suppress the printing of the panic message to the console, resulting in unclean test execution log
  • it is not possible to add additional checks after the panic has happened

As a better alternative, I would highly recommend checking out the library called fluent-asserter

By using it, you can easily write an assertion that checks if a panic has occurred, as follows:

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

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

The good thing with this is that:

  • it uses a fluent interface, resulting in a readable assertion
  • it suppresses the printing of the panic message to the console, resulting in a clean test execution log
  • you can add additional assertions after the panic check

Upvotes: 2

Vladimir Matveev
Vladimir Matveev

Reputation: 128111

You can find the answer in testing section of the Rust book. More specifically, you want #[should_panic] attribute:

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

Upvotes: 251

k06a
k06a

Reputation: 18825

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
}

Upvotes: 13

Francis Gagn&#233;
Francis Gagn&#233;

Reputation: 65937

If you want to assert that only a specific portion of the test function fails, use std::panic::catch_unwind() and check that it returns an Err, for example with is_err(). In complex test functions, this helps ensure that the test doesn't pass erroneously because of an early failure.

Several tests in the Rust standard library itself use this technique.

Upvotes: 33

m00am
m00am

Reputation: 6298

As an addendum: The solution proposed by @U007D also works in doctests:

/// 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
    }
}

Upvotes: 6

Related Questions