Unit Testing

#[should_panic]

The #[should_panic] attribute inverts the pass/fail condition of a #[test] function: the test passes only if the function panics during execution, and fails if the function returns normally without panicking.

#[test]
#[should_panic(expected = "substring")]
fn my_test()
  |
  +-- Function returns normally  --> TEST FAILS ("note: test did not panic as expected")
  +-- Panics with wrong message  --> TEST FAILS (shows expected vs actual panic message)
  +-- Panics matching substring  --> TEST PASSES ("ok")

Bare #[should_panic] vs #[should_panic(expected = "...")]

A bare #[should_panic] attribute passes if any panic occurs anywhere inside the test function—even if the panic happened in an unrelated setup line before the function under test was called.

Adding expected = "..." restricts the test so that it only passes if the panic message contains the specified substring. Always prefer #[should_panic(expected = "...")] so unrelated bugs in test setup cannot masquerade as a passing test.

Attribute Form Passes When Risk
#[should_panic] Any panic occurs in the test body High: an accidental unwrap() failure in setup passes the test
#[should_panic(expected = "msg")] Panic message contains "msg" as a substring Low: verifies the specific failure reason
pub struct Percentage(u8);

impl Percentage {
    pub fn new(value: u8) -> Self {
        assert!(
            value <= 100,
            "Percentage out of range: expected 0..=100, got {value}"
        );
        Self(value)
    }

    pub fn value(&self) -> u8 {
        self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "Percentage out of range: expected 0..=100, got 125")]
    fn rejects_values_above_100() {
        let _ = Percentage::new(125);
    }
}

fn main() {
    let valid = Percentage::new(85);
    println!("Valid percentage: {}%", valid.value());
}
Valid percentage: 85%

Running cargo test verifies that rejects_values_above_100 panics with the expected substring:

running 1 test
test tests::rejects_values_above_100 ... ok

test result: ok. 1 passed; 0 failed; 0 filtered out; finished in 0.00s

Diagnostic Output When expected Does Not Match

If a #[should_panic(expected = "...")] test panics with a message that does not contain the expected substring, cargo test fails the test and prints both strings:

---- tests::rejects_values_above_100 stdout ----
thread 'tests::rejects_values_above_100' panicked at src/lib.rs:6:9:
Percentage out of range: expected 0..=100, got 101
note: panic did not contain expected string
      panic message: `"Percentage out of range: expected 0..=100, got 101"`,
 expected substring: `"got 125"`

Incompatible with Result Return Types

A #[should_panic] test must return (). Combining #[should_panic] with fn test() -> Result<(), E> is rejected at compile time:

// ❌ does not compile
#[test]
#[should_panic]
fn cannot_return_result() -> Result<(), String> {
    panic!("boom");
}
error: functions using `#[should_panic]` must return `()`
 --> src/lib.rs:4:1
  |
4 | fn cannot_return_result() -> Result<(), String> {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^