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

  1. Add a test for a function that returns the larger of two numbers.
  2. Mark a test that should panic with #[should_panic] and check the summary.
  3. 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.

  1. 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.

  2. 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.

  3. What does cargo test divides do?

    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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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

Banner image