RustArena

Unit testing in Rust: #[test], assert_eq! and cargo test

How to write tests in Rust: #[test] functions, assert!, assert_eq! with messages, should_panic, tests returning Result, and unit vs integration tests.

Rust has testing built in: no framework to install. A test is a function marked #[test]; cargo test compiles your crate in test mode, runs every test, and reports which passed.

Unit tests live next to the code

pub fn apply_discount(price_cents: u64, percent: u64) -> u64 {
    price_cents - price_cents * percent / 100
}

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

    #[test]
    fn ten_percent_off() {
        assert_eq!(apply_discount(1000, 10), 900);
    }

    #[test]
    fn zero_percent_changes_nothing() {
        assert_eq!(apply_discount(1999, 0), 1999, "0% should keep the price");
    }
}
$ cargo test
running 2 tests
test tests::ten_percent_off ... ok
test tests::zero_percent_changes_nothing ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

cargo test discount runs only tests whose name contains "discount". Output from println! in passing tests is hidden; cargo test -- --nocapture shows it.

The assertion macros

Macro Fails when
assert!(cond) cond is false
assert_eq!(left, right) they're not equal; prints both values
assert_ne!(left, right) they're equal

All accept a trailing message with format arguments: assert_eq!(total, 30, "cart total for {items:?}"). The values compared with assert_eq! must implement PartialEq and Debug, so derive both on your types. Floats rarely compare exactly; assert that the difference is small: assert!((a - b).abs() < 1e-9).

Testing failures

Integration tests and doc tests

Good tests at work

Common mistakes

Example

pub fn apply_discount(price_cents: u64, percent: u64) -> u64 {
    assert!(percent <= 100, "percent must be at most 100, got {percent}");
    price_cents - price_cents * percent / 100
}

#[derive(Debug, PartialEq)]
pub enum ParseError {
    Empty,
    NotANumber(String),
}

pub fn parse_qty(s: &str) -> Result<u32, ParseError> {
    let s = s.trim();
    if s.is_empty() {
        return Err(ParseError::Empty);
    }
    s.parse().map_err(|_| ParseError::NotANumber(s.to_string()))
}

fn main() {
    println!("{}", apply_discount(1000, 10));
    println!("{:?}", parse_qty(" 7 "));
    println!("{:?}", parse_qty(""));
    println!("{:?}", parse_qty("seven"));
}

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

    #[test]
    fn ten_percent_off() {
        assert_eq!(apply_discount(1000, 10), 900);
    }

    #[test]
    fn full_discount_is_free() {
        assert_eq!(apply_discount(1999, 100), 0, "100% off should cost nothing");
    }

    #[test]
    #[should_panic(expected = "at most 100")]
    fn rejects_discount_over_100() {
        apply_discount(1000, 150);
    }

    #[test]
    fn parses_with_whitespace() -> Result<(), ParseError> {
        assert_eq!(parse_qty(" 42\n")?, 42);
        Ok(())
    }

    #[test]
    fn reports_empty_and_bad_input() {
        assert_eq!(parse_qty("   "), Err(ParseError::Empty));
        assert_eq!(parse_qty("4x"), Err(ParseError::NotANumber("4x".to_string())));
    }
}

Output:

900
Ok(7)
Err(Empty)
Err(NotANumber("seven"))

Practice it