Unit tests with cargo test
A unit test is a function marked #[test] in the same crate as the code it tests. Cargo finds it, runs it in its own thread, and reports a pass or a failure with the assertions that failed. There is no framework to install and no test file to register.
The lesson builds one package and grows its test module: one test, then three, then a filtered run, then a failing one. Every output is the real cargo test output from this machine, and the last one is a failure on purpose. The comparison page for this lesson writes the same checks in C++.
A first test
The package is a library with one function and one test. The test module is compiled only for a test build, and use super::* brings the function under test into scope.
The manifest is the one cargo new --lib writes, and cargo test compiles the crate with the tests and runs them.
cargo new --vcs none --lib calc
vi src/lib.rs
cargo test
[package]
name = "calc"
version = "0.1.0"
edition = "2024"
[dependencies]
pub fn add(left: i32, right: i32) -> i32 {
left + right
}
pub fn divide(left: i32, right: i32) -> i32 {
left / right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn divides_exactly() {
assert!(divide(10, 2) == 5, "ten divided by two must be five");
}
#[test]
#[should_panic]
fn divides_by_zero() {
divide(1, 0);
}
}
cargo test
Compiling calc v0.1.0 (/tmp/rw5/L065/calc)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.13s
Running unittests src/lib.rs (target/debug/deps/calc-c10086937597dec3)
running 1 test
test tests::adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests calc
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The output names the test and says ok, then counts the pass. The doc-tests section is empty because the library has no examples in its documentation.
A module of tests
A second function gets its own test, and the #[should_panic] attribute marks a test that passes only when the code panics. The three tests share the module and the import.
The program divides ten by two, asserts a condition with a message, and calls a division by zero that must panic. Cargo reports three passes.
pub fn add(left: i32, right: i32) -> i32 {
left + right
}
pub fn divide(left: i32, right: i32) -> i32 {
left / right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn divides_exactly() {
assert!(divide(10, 2) == 5, "ten divided by two must be five");
}
#[test]
#[should_panic]
fn divides_by_zero() {
divide(1, 0);
}
}
cargo test
Compiling calc v0.1.0 (/tmp/rw5/L065/calc)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.50s
Running unittests src/lib.rs (target/debug/deps/calc-c10086937597dec3)
running 3 tests
test tests::adds_two_numbers ... ok
test tests::divides_exactly ... ok
test tests::divides_by_zero - should panic ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests calc
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The should-panic test passes because the divide by zero panicked as expected. A test that panics without the attribute is a failure, which the last section shows.
Running a subset
cargo test takes a name filter after the command, and only the tests whose names contain it run. The summary counts the rest as filtered out, which is how a large suite is kept quick while working on one area.
The command below runs the two tests with divides in their names. The output says two passed and one filtered out.
cargo test divides
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.06s
Running unittests src/lib.rs (target/debug/deps/calc-c10086937597dec3)
running 2 tests
test tests::divides_exactly ... ok
test tests::divides_by_zero - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
The filter matches the full test name, including the module path, so a module name works as a filter too. The next section is the failure output a filter does not change.
A failing test
The assertion expects five where the function returns four. Cargo reports the test as failed, prints the panic block with the two values and ends with a non-zero status.
The output names the test, shows the left and right values and repeats the failing test at the end, which is the summary a continuous integration log keeps.
pub fn add(left: i32, right: i32) -> i32 {
left + right
}
pub fn divide(left: i32, right: i32) -> i32 {
left / right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn divides_exactly() {
assert!(divide(10, 2) == 5, "ten divided by two must be five");
}
#[test]
#[should_panic]
fn divides_by_zero() {
divide(1, 0);
}
}
cargo test adds
Compiling calc v0.1.0 (/tmp/rw5/L065/calc)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/calc-c10086937597dec3)
running 1 test
test tests::adds_two_numbers ... FAILED
failures:
---- tests::adds_two_numbers stdout ----
thread 'tests::adds_two_numbers' (1983) panicked at src/lib.rs:15:9:
assertion `left == right` failed
left: 4
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::adds_two_numbers
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
The test name in the panic line includes the module, which is how a failure points at the test rather than at the function. The lesson on testing files and time makes the same output independent of the machine.
Exercises
- Add a test for a function that returns the larger of two numbers.
- Mark a test that should panic with #[should_panic] and check the summary.
- Run only the tests whose names contain your function name.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
How does cargo find a unit test?
Answer
It runs every function marked #[test]
The attribute registers the function with the test harness at compile time.
-
What does #[should_panic] mean?
Answer
The test passes only if the code panics
The attribute inverts the expectation for a test that checks a panic.
-
What does
cargo test dividesdo?Answer
Runs the tests whose names contain divides
The argument is a name filter, and the rest of the suite is reported as filtered out.
-
What does a failing assert_eq! show in the test output?
Answer
The left and right values
The macro prints both sides so the diff is visible in the log.
-
What does the test module need to call the function under test?
Answer
use super::*
The tests module is a child of the crate root, and super brings the items into scope.
-
What does a passing test line look like?
Answer
test tests::adds_two_numbers ... ok
The harness prefixes the module path with test and ends the line with ok.
-
What does the summary line of a successful run say?
Answer
test result: ok. 1 passed; 0 failed; 0 ignored
The harness counts passed, failed and ignored tests in one line.
-
What does the #[cfg(test)] attribute do?
Answer
Compiles the module only for a test build
The attribute keeps the test code out of a normal build.
-
What happens when a test panics without #[should_panic]?
Answer
Cargo reports the test as failed
A panic is a failure unless the test expects one, and the run ends with a non-zero status.
-
Which command builds the tests without running them?
Answer
cargo test --no-run
The flag compiles the test harness and stops before the run.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and tested on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev