Unit Testing

Testing Fundamentals

Automated software testing verifies that code behaves as expected across normal inputs, boundary conditions, and error cases. Rust includes a built-in test framework directly in rustc and cargo, requiring no external testing libraries to write and execute unit tests, integration tests, and documentation tests.

+-------------------------------------------------------------------+
|                     Rust Testing Pyramid                          |
|                                                                   |
|        / \          Documentation Tests (`/// ``` ... ````)       |
|       /   \         Verifies public API examples stay runnable    |
|      /-----\                                                      |
|     /       \       Integration Tests (`tests/*.rs`)              |
|    /         \      Tests public crate API as an external caller  |
|   /-----------\                                                   |
|  /             \    Unit Tests (`#[cfg(test)] mod tests`)         |
| /_______________\   Tests internal functions & modules in-place   |
+-------------------------------------------------------------------+

Categories of Tests in Rust

Test Category Location Visibility Access Compilation Model
Unit Tests Inside src/*.rs (#[cfg(test)] mod tests) Public and private items in the module hierarchy Compiled into the crate's test binary via rustc --test
Integration Tests Separate tests/*.rs files at package root Public (pub) items only (links crate as external dependency) Each tests/*.rs file compiles into its own standalone test binary
Documentation Tests Code blocks inside /// or //! doc comments Public (pub) items only Extracted and compiled by rustdoc --test (for library targets)

How rustc --test Works

When you run cargo test, Cargo compiles your crate with the --test flag:

  1. The compiler enables the cfg(test) conditional compilation predicate.
  2. It collects every function annotated with #[test] across the crate.
  3. Instead of calling fn main(), it synthesizes a dedicated test harness runner (libtest) that spawns threads to execute tests in parallel, captures their standard output, catches panics, and reports a summary of passed, failed, and ignored tests.
pub fn clamp_percentage(value: i32) -> u8 {
    value.clamp(0, 100) as u8
}

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

    #[test]
    fn clamps_within_bounds() {
        assert_eq!(clamp_percentage(-25), 0);
        assert_eq!(clamp_percentage(42), 42);
        assert_eq!(clamp_percentage(180), 100);
    }
}

fn main() {
    println!("Clamped 180 -> {}%", clamp_percentage(180));
}
Clamped 180 -> 100%

Running cargo test compiles the test harness and executes tests::clamps_within_bounds:

running 1 test
test tests::clamps_within_bounds ... ok

test result: ok. 1 passed; 0 failed; 0 filtered out; finished in 0.00s

Pass and Failure Semantics

A Rust test passes or fails based on two rules:

  • Returning (): The test passes if the function returns normally without panicking, and fails if the thread panics (for example, via assert!, assert_eq!, panic!, or .unwrap()).
  • Returning Result<(), E>: The test passes if it returns Ok(()), and fails if it returns Err(err) or panics.