Testing

Rust · Practical · 12 min

Rust ships testing in the toolchain, and well-tested crates are the norm in the ecosystem.

A test is a function annotated #[test]. It passes if it returns normally and fails if it panics. Assertions are macros: assert!(cond), assert_eq!(left, right), assert_ne!, each with optional format arguments for a custom message. assert_eq! prints both values on failure, so the compared types must implement Debug and PartialEq.

Unit tests conventionally live in the same file as the code, in a child module:

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn works() { assert_eq!(add(2, 2), 4); }
}

#[cfg(test)] compiles the module only for cargo test, and because it is a child module it can test private functions.

Integration tests go in the tests/ directory. Each file there is a separate crate that uses your library like an external user would, through its public API only.

Doc tests: code blocks in /// documentation comments are compiled and run by cargo test, which keeps examples in your docs honest.

Other tools:

  • A test can return Result<(), E> and use ?; returning Err fails it.
  • #[should_panic(expected = "divide by zero")] asserts a panic with matching message.
  • #[ignore] skips slow tests unless you run cargo test -- --ignored.
  • cargo test parse runs only tests whose names contain parse; cargo test -- --nocapture shows println! output.

Tests run in parallel threads by default, so tests that touch shared state like files or environment variables need unique paths or --test-threads=1. Popular additions include proptest for property testing and insta for snapshot tests.

Example

pub fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 { return Err("divide by zero".into()); }
    Ok(a / b)
}

fn clamp_pct(x: i32) -> i32 { x.clamp(0, 100) } // private, still testable

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn divides() { assert_eq!(divide(7, 2), Ok(3), "integer division truncates"); }

    #[test]
    fn clamps() { assert!(clamp_pct(150) == 100); }
    #[test]
    fn with_question_mark() -> Result<(), String> {
        assert_ne!(divide(10, 5)?, 0);
        Ok(())
    }

    #[test]
    #[should_panic(expected = "divide by zero")]
    fn zero_panics() { divide(1, 0).unwrap(); }
}

Remember

  • #[test] functions pass unless they panic; use assert_eq! for good failure output
  • Unit tests sit in a #[cfg(test)] mod tests and can reach private items
  • tests/ holds integration tests; /// examples run as doc tests

Watch out for

  • Tests that share a temp file or env var and fail randomly because tests run in parallel.

Check yourself

  1. Where should a test go that should only exercise your crate's public API?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch11-01-writing-tests.html

Builds on

Unlocks

Nothing builds on this yet.