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_addsTests 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?