24.5 Testing Panics and Errors

Sometimes, the expected behavior of code under specific conditions is to panic or return an error. Rust’s test framework provides ways to verify this.

24.5.1 Expecting Panics with #[should_panic]

If a function is designed to panic for certain inputs (e.g., division by zero, out-of-bounds access on a custom type), you can use the #[should_panic] attribute on a test function. The test passes if the code inside panics and fails if it completes without panicking.

pub fn get_element(slice: &[i32], index: usize) -> i32 {
    // This will panic if index is out of bounds
    slice[index]
}

#[test]
#[should_panic]
fn test_index_out_of_bounds() {
    let data = [1, 2, 3];
    get_element(&data, 5); // Accessing index 5 should panic
}

To make the test more specific, you can assert that the panic message contains a certain substring using the expected parameter. This helps ensure the code panics for the intended reason.

#[test]
#[should_panic(expected = "out of bounds")]
fn test_specific_panic_message() {
    let data = [1, 2, 3];
    get_element(&data, 5); // Panics with a message like "index out of bounds: the len is 3 but the index is 5"
}

This test passes only if the function panics and the panic message includes the substring “out of bounds”.

24.5.2 Using Result<T, E> in Tests

Test functions can return Result<(), E> instead of (). This allows the use of the question mark operator (?) within the test for cleaner handling of operations that return Result.

  • The test passes if it returns Ok(()).
  • The test fails if it returns an Err(E).
  • The error type E must implement the std::fmt::Debug trait so the test runner can print it upon failure.
use std::num::ParseIntError;

// Function that might return an error
fn parse_even_number(s: &str) -> Result<i32, ParseIntError> {
    let number = s.parse::<i32>()?; // Propagate ParseIntError if parsing fails
    if number % 2 == 0 {
        Ok(number)
    } else {
        // For simplicity, we reuse ParseIntError, though a custom error type is often better.
        // This specific error construction is illustrative; typically you'd define a custom error enum.
        Err("".parse::<i32>().unwrap_err()) // Create a dummy ParseIntError for odd numbers
    }
}

#[test]
fn test_parse_valid_even() -> Result<(), ParseIntError> {
    let number = parse_even_number("42")?; // Use `?` - test proceeds if Ok
    assert_eq!(number, 42);
    Ok(()) // Return Ok(()) to indicate success
}

#[test]
fn test_parse_odd_returns_err() {
    // We expect an Err, so we don't use `?` or return Result
    let result = parse_even_number("3");
    assert!(result.is_err());
    // Optionally, check the specific error kind if needed
}

#[test]
fn test_parse_invalid_string_fails() -> Result<(), ParseIntError> {
    // This test will fail because parse_even_number returns Err("abc".parse()?)
    // The Err will propagate out, causing the test runner to mark it as failed.
    let _number = parse_even_number("abc")?;
    Ok(()) // This line is never reached
}

Note: You cannot use the #[should_panic] attribute on a test function that returns Result. If you need to test that a function returning Result specifically produces an Err variant, assert this directly using methods like is_err(), unwrap_err(), or pattern matching, as shown in test_parse_odd_returns_err.