A bare #[should_panic] passes on any panic, not the one you meant. If the code under test panics for a different reason than the one you were guarding against, the test stays green — and the guard you thought you had never existed. The page's own example sets expected = ...; leave it off and a test can pass while proving nothing.
pub fn average(items: &[i32]) -> i32 {
items.iter().sum::<i32>() / items.len() as i32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn empty_input_panics() {
average(&[]);
}
}
// cargo test: test tests::empty_input_panics - should panic ... ok
// it passes -- but only because the division panics with "attempt to divide by zero";
// there is no empty-input check at allA function annotated #[test] is automatically discovered and run by cargo test. Inside a test, the assert!, assert_eq!, and assert_ne! macros check conditions and panic with a diagnostic message if they fail, which cargo test reports as a failed test rather than crashing the whole test run.
A basic test with #[test]
Tests conventionally live in a tests submodule marked #[cfg(test)], which excludes them from a normal (non-test) build entirely — test code adds nothing to the shipped binary.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*; // brings the parent module's items into scope
#[test]
fn it_adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}Run with cargo test, which discovers and runs every #[test] function in the crate.
assert! macros and #[should_panic]
assert_eq!/assert_ne! print both values on failure, which is more useful for debugging than a bare assert!. #[should_panic] flips the expected outcome — the test passes only if the code inside DOES panic.
#[test]
fn it_fails_with_a_clear_message() {
assert_eq!(add(2, 2), 5); // fails, prints: left: 4, right: 5
}
#[test]
#[should_panic(expected = "divide by zero")]
fn it_panics_on_bad_input() {
divide(10, 0); // test PASSES only because this panics
}That's what the expected argument above is for. Pinning the test to the message you intended makes it fail until the function really has that guard — the failure output shows the actual panic message next to the expected one. Adding the missing check turns it green for the right reason:
pub fn average(items: &[i32]) -> i32 {
assert!(!items.is_empty(), "empty input");
items.iter().sum::<i32>() / items.len() as i32
}
#[test]
#[should_panic(expected = "empty input")]
fn empty_input_panics() {
average(&[]);
}
// without the assert!, this test FAILS:
// panic message: "attempt to divide by zero"
// expected substring: "empty input"
// with it: test tests::empty_input_panics - should panic ... okTesting Result-returning functions
A test function can itself return Result<(), E> and use the ? operator, letting you write tests using the same error-propagation style as the rest of the codebase instead of unwrap-ing everywhere.
#[test]
fn it_works_with_result() -> Result<(), String> {
if add(2, 2) == 4 {
Ok(())
} else {
Err(String::from("two plus two doesn't equal four"))
}
}