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.
| Macro | Passes 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 timeRust 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 testbefore every commit.
Key Points
#[test]marks a test function, andcargo testruns all of them.assert!,assert_eq!, andassert_ne!state expectations.#[should_panic]checks that bad input triggers a panic.- Unit tests live inside source files, and integration tests live in the
testsfolder.
