14.2Writing tests
Time to write a test and watch it pass. A Rust test is just a function with an attribute on it; cargo test finds every such function, runs it, and reports the results. This lesson covers the mechanics: the #[test] attribute, the assertion macros that make a test check something, and the two special forms for testing panics and Results.
The #[test] attribute
A test is a function marked with the #[test] attribute (the #[...] syntax from lesson 10.7). Here's a function and a test for it in the same file:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[test]
fn adds_two_numbers() {
let result = add(2, 3);
assert_eq!(result, 5);
}
Run cargo test and the framework finds adds_two_numbers, runs it, and checks the assertion:
running 1 test
test adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The function takes no arguments and returns nothing (usually). #[test] is what marks it as a test rather than ordinary code, so a normal cargo build ignores it and only cargo test compiles and runs it. A test passes if it runs to the end without panicking, and fails if anything in it panics, which is why the assertion macros work by panicking on failure.
The assert! family
Three macros do almost all the checking. assert!(condition) passes if the condition is true and panics if it's false:
#[test]
fn five_is_positive() {
assert!(5 > 0);
}
assert_eq!(a, b) passes if a == b, and on failure panics and prints both values, which is far more useful than a bare assert!(a == b) that would only tell you "false." assert_ne!(a, b) is the opposite, passing when the two differ:
#[test]
fn arithmetic_works() {
assert_eq!(add(2, 2), 4);
assert_ne!(add(2, 2), 5);
}
When assert_eq! fails, the output shows what it got versus what it expected:
running 1 test
test arithmetic_works ... FAILED
failures:
---- arithmetic_works stdout ----
thread 'arithmetic_works' panicked at src/lib.rs:4:5:
assertion `left == right` failed
left: 4
right: 5
That left: 4, right: 5 is why you reach for assert_eq! over assert!: it tells you not just that the check failed but how, which is most of the way to the fix. (assert_eq! requires the values to be comparable with == and printable for the failure message; for your own types that means deriving PartialEq and Debug, the derives from lesson 10.7, with the full story in chapter 16.)
Best practice
Prefer assert_eq!/assert_ne! over assert!(a == b) whenever you're comparing values, so failures show both sides. Reach for plain assert! only when the check is a genuine boolean that isn't a comparison (assert!(list.is_empty()), assert!(result.is_ok())). You can add a custom message to any of them: assert!(x > 0, "x was {x}, expected positive").
Testing that something panics
Sometimes the correct behavior is a panic: a function that's supposed to reject bad input by panicking (lesson 12.1). You test that with #[should_panic], which inverts the pass condition, so the test passes only if the body does panic:
pub fn checked_age(age: i32) -> i32 {
if age < 0 {
panic!("age cannot be negative");
}
age
}
#[test]
#[should_panic]
fn rejects_negative_age() {
checked_age(-5);
}
rejects_negative_age passes because checked_age(-5) panics as intended. You can be stricter and require the panic message to contain specific text, which guards against the test passing for the wrong panic: #[should_panic(expected = "cannot be negative")]. Without expected, any panic counts, which can mask a different bug.
Tests that return Result
A test can return Result<(), E> instead of nothing, which lets you use the ? operator (lesson 12.3) inside it. The test passes on Ok(()) and fails on Err:
#[test]
fn parses_a_number() -> Result<(), std::num::ParseIntError> {
let n: i32 = "42".parse()?;
assert_eq!(n, 42);
Ok(())
}
This is handy when a test does several fallible steps: instead of unwrapping each, you ? them, and any error fails the test with the error attached. Use the plain (non-Result) form for ordinary assertions and the Result form when ? would save you a pile of unwraps. Note that with a Result-returning test you can't use #[should_panic]; to check for an Err, assert on it directly (assert!(result.is_err())).
Quiz time
Question #1
What attribute marks a function as a test, when does a test pass, and when does it fail?
Show solution
#[test] marks a test function. It passes if it runs to completion without panicking, and fails if anything in it panics, which is how the assertion macros report failure (they panic when the check fails).
Question #2
Why is assert_eq!(a, b) usually better than assert!(a == b)?
Show solution
On failure, assert_eq! prints both values (left: 4, right: 5), telling you not just that the check failed but what the actual and expected values were. assert!(a == b) only reports that a boolean was false, giving you no information about the values. The extra detail from assert_eq! is most of the way to diagnosing the failure.
Question #3
Write a test verifying that a function sqrt_checked(n: i32) panics when given a negative number, and require the panic message to mention "negative".
Show solution
#[test]
#[should_panic(expected = "negative")]
fn rejects_negative() {
sqrt_checked(-1);
}
#[should_panic(expected = "negative")] makes the test pass only if the body panics and the panic message contains "negative", guarding against the test passing for an unrelated panic.
Your tests so far have sat loose in the file. The next lesson covers the conventional way to organize unit tests, the #[cfg(test)] mod tests block, and tackles the question of whether (and how) to test private functions.