🎓 A1Learner
🔍

Rust Testing

A test is a small program that checks code automatically. Tests run again in one command after every change.

Tests live in their own module

#[cfg(test)] means the module below is compiled only while testing. Unit tests sit next to the code, so they can see private items too.

#[cfg(test)]
mod tests {
    use super::*;
    // tests go here
}

A test is a function with #[test]

The test passes if it finishes. It fails if it panics — and an assert that fails panics. Every test follows three steps: set up the data, run the code, then check the result.

#[test]
fn it_adds() {
    assert!(add(2, 3) > 4);
    assert_eq!(add(2, 3), 5);
}

A test that must panic

Some code should fail. #[should_panic] passes only when the code inside panics:

#[test]
#[should_panic]
fn third_score_missing() {
    let scores = vec![10, 20];
    let _third = scores[5]; // panics: there is no third score
}

Skip a slow test for now

#[ignore] leaves a test out of normal runs, until you ask for it by name or with --ignored:

#[test]
#[ignore]
fn huge_file_search() {
    // slow test, skipped for now
}

Run one test by name

A word after cargo test is a filter: only tests whose names contain it will run. The Running Tests lesson shows the full control panel.

cargo test it_adds

Tests outside the crate

A file in the tests/ folder is its own crate. It imports your library. It uses only the public API, exactly like a real user. Shared helper code goes in tests/common/mod.rs, where it is not mistaken for a test file. Only library crates can be tested this way, so keep main.rs thin and the real logic in lib.rs.

// tests/add_from_outside.rs
use my_library::add;

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

A test that returns Result

Return Ok(()) to pass. Return an Err and the test fails, with that error as its report:

#[test]
fn parses_a_number() -> Result<(), String> {
    let n = parse_number("42")?;
    assert_eq!(n, 42);
    Ok(())
}

The ? shortcut inside a test

Use ? inside such a test: the first Err stops the test and fails it, so the happy path stays short:

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

When the error is the point

Check a success before you rely on its value, and an expected failure by asking the Result directly. Never use ? to check an expected error — ? would turn that error into a failure of the test itself:

#[test]
fn rejects_words() {
    assert!(parse_number("abc").is_err());
    assert!(parse_number("42").is_ok());
}

Result tests in one program

In this example, parse_number turns text into a number, or fails with a message. The first test returns Result<(), String>. So it can put ? after parse_number("42"). If parsing failed, the test would stop and fail with that error. Instead it gets 42. assert_eq! confirms it. And Ok(()) ends the test as a pass. The second test checks the failure path on purpose: parse_number("abc") must be an Err, and is_err() says so. main parses the same good input and prints parsed = 42.

fn parse_number(text: &str) -> Result<i32, String> { text.trim().parse().map_err(|_| format!("not a number: {text}")) }
#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn parses_a_number() -> Result<(), String> { assert_eq!(parse_number("42")?, 42); Ok(()) }
    #[test] fn rejects_words() { assert!(parse_number("abc").is_err()); }
}
fn main() { match parse_number("42") { Ok(n) => println!("parsed = {n}"), Err(e) => println!("error: {e}"), } }

Example

fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn it_adds() { assert_eq!(add(2, 3), 5); }
    #[test] fn negatives() { assert_eq!(add(-1, 1), 0); }
}
fn main() { println!("add(2,3) = {} — run cargo test for 2 passing tests", add(2, 3)); }

Key points

  • A test runs code and checks the result without a person watching.
  • Examples inside doc comments also run as tests, called doctests
  • A test fails when it panics.
  • Each test runs in its own thread.
  • So one failure does not stop the others.
  • Use #[ignore = "reason"] to write down why a test is skipped.
  • The reason prints beside the test name in the run report.
  • Tests add no weight to your real program.
  • Rust treats tests as a first-class part of the language, not an extra.
  • Tests do the checking job better than printing values by hand.
  • For code that should return Err, assert is_err() instead.
  • A Result test passes by returning Ok and fails by returning Err.

In this example, add is a plain function. Below it sits a tests module, marked #[cfg(test)] so it only exists while testing. Inside are two tests: it_adds checks add(2, 3) equals 5, and negatives checks add(-1, 1) equals 0. use super::* imports the add function from the parent module. The main function just prints the sum. It reminds you that cargo test is where the real checking happens. A real run reports "2 passed".

Note

cfg(test) means the test module is compiled only during cargo test.

You cannot combine #[should_panic] with a test that returns Result.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Where do integration tests live, and what can they access?