Testing in Rust

Most languages treat testing as an afterthought — a separate framework you bolt on, a separate directory, a separate mental mode. Rust treats it as a first-class, built-in feature: testing is part of the language and its tooling, you write tests right next to the code they test, and `cargo test` just works. This tight integration, combined with Rust's culture of correctness, makes testing in Rust unusually pleasant and encourages a habit that pairs perfectly with the compiler's guarantees.

This post covers testing in Rust — a first-class, built-in feature. It covers writing tests with the #[test] attribute, the assertion macros, running tests with cargo test, and the distinction between unit and integration tests. Rust’s testing is integrated into the language and Cargo (from Module 1), making it low-friction and encouraging good testing habits. Combined with the compiler’s guarantees, tests complete Rust’s correctness story.

Writing tests

A Rust test is just a function marked with the #[test] attribute. It passes if it runs without panicking, fails if it panics:

// A function to test.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn adds_two_numbers() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn adds_negatives() {
        assert_eq!(add(-1, -1), -2);
    }
}

A Rust test is a #[test]-annotated function that passes if it doesn’t panic and fails if it does; unit tests live alongside the code in a #[cfg(test)] module (compiled only for tests), and testing is built into the language and Cargo — no external framework. Tests express expectations through assertion macros.

Assertion macros

Rust provides assertion macros to check conditions in tests — a test fails (panics) if an assertion fails:

#[test]
fn assertions_demo() {
    let result = add(2, 2);
    assert_eq!(result, 4);              // fails and prints values if not equal
    assert!(result > 0);               // fails if the condition is false
    assert_ne!(result, 5);             // fails if equal
    assert_eq!(result, 4, "add(2,2) should be 4, got {result}"); // custom message
}

Rust’s assertion macros — assert! (boolean), assert_eq!/assert_ne! (equality, printing values on failure), with optional custom messages — express test expectations, and #[should_panic] and Result-returning tests handle panic/error cases. These make writing clear, debuggable tests easy. Running them is equally simple.

Running tests with cargo test

cargo test (from Cargo, Module 1) builds and runs all your tests — one command, integrated into the tooling:

cargo test runs your whole test suite with one integrated command — compiling in test mode, running all #[test] functions in parallel (so keep tests independent), reporting pass/fail with helpful failure output, and supporting name-based filtering. Its low friction makes testing a natural habit. Beyond unit tests, Rust distinguishes integration tests.

Unit vs integration tests

Rust distinguishes unit tests (testing internals, alongside the code) from integration tests (testing the public API, from outside) — a useful separation:

Rust distinguishes unit tests (internal, alongside the code, can test private items — for detailed component correctness) from integration tests (in tests/, external, testing the public API as a user would — for end-to-end behavior), plus doc tests (examples in doc comments run as tests). cargo test runs all of these, giving comprehensive, low-friction testing. Combined with the compiler’s guarantees, tests complete Rust’s correctness story. Next: error-handling libraries (anyhow and thiserror).

Key takeaways

Further reading

Sources & References