Rust Writing Tests

Tests are small programs that check whether your code behaves correctly. A test works like a taste check while cooking: you try a spoonful before serving the whole pot. Rust includes a test system in the language itself, so no extra tools are needed.

A First Test

Mark a function with the #[test] attribute, and Cargo treats it as a test. A test passes when the function ends normally and fails when it panics.

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);
    }
}

The attribute #[cfg(test)] compiles the module only during testing, so the final program carries no test code. The line use super::*; brings the parent module's functions into the test module.

Running Tests

Type one command in the project folder.

cargo test
running 1 test
test tests::adds_two_numbers ... ok

test result: ok. 1 passed; 0 failed

Assertion Macros

Assertions state what must be true.

MacroPasses When
assert!(condition)The condition is true
assert_eq!(left, right)Both values are equal
assert_ne!(left, right)The values differ
#[test]
fn checks_ranges() {
    let age = 20;
    assert!(age >= 18);
    assert_ne!(age, 0);
}

Custom Failure Messages

Extra arguments become part of the failure report and speed up diagnosis.

assert_eq!(result, 10, "Expected 10 but received {}", result);

Seeing a Failure

Imagine a function with a bug.

fn double(n: i32) -> i32 {
    n * 3   // bug
}

#[test]
fn doubles_a_number() {
    assert_eq!(double(4), 8);
}
---- tests::doubles_a_number stdout ----
assertion `left == right` failed
  left: 12
 right: 8

The report shows both values side by side, which points straight to the faulty calculation.

Testing for Panics

Some code should panic on bad input. The attribute #[should_panic] marks a test that passes only when a panic occurs.

fn percent(part: u32, total: u32) -> u32 {
    if total == 0 {
        panic!("total cannot be zero");
    }
    part * 100 / total
}

#[test]
#[should_panic(expected = "total cannot be zero")]
fn rejects_zero_total() {
    percent(5, 0);
}

Tests That Return Result

A test can return Result and use the question mark operator. An Err value fails the test.

#[test]
fn parses_number() -> Result<(), std::num::ParseIntError> {
    let n: i32 = "42".parse()?;
    assert_eq!(n, 42);
    Ok(())
}

Controlling Which Tests Run

cargo test adds              # tests whose names contain "adds"
cargo test -- --show-output  # show println! text from passing tests
cargo test -- --test-threads=1   # run one test at a time

Rust runs tests in parallel by default. Tests that share files or other resources may need the single-thread option.

Ignoring Slow Tests

Add #[ignore] to skip a slow test during normal runs. Run cargo test -- --ignored to include it on demand.

Unit Tests and Integration Tests

Two layers of testing exist.

 my_project/
 |-- src/
 |    |-- lib.rs        unit tests live here, inside #[cfg(test)]
 |-- tests/
      |-- api_test.rs   integration tests live here

Unit tests sit beside the code and can reach private functions. Integration tests sit in a tests folder, and each file acts as a separate crate that uses only the public interface.

// tests/api_test.rs
use my_project::add;

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

Documentation Tests

Code examples inside /// comments run as tests too. The examples stay accurate because a broken example fails the build.

/// Adds two numbers.
///
/// ```
/// assert_eq!(my_project::add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Good Testing Habits

  • Give each test one clear purpose and a descriptive name.
  • Cover normal cases, edge cases, and error cases.
  • Keep tests independent so they pass in any order.
  • Run cargo test before every commit.

Key Points

  • #[test] marks a test function, and cargo test runs all of them.
  • assert!, assert_eq!, and assert_ne! state expectations.
  • #[should_panic] checks that bad input triggers a panic.
  • Unit tests live inside source files, and integration tests live in the tests folder.