Testing & Benchmarking
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0, the 2024 edition, criterion 0.8, proptest 1.11 and insta 1.49, checked on
2026-10-06. Rust has testing built into the language and Cargo: annotate a function with #[test], run cargo test, and
Cargo builds a test binary, runs every test in parallel and reports the results. This page covers the built-in harness,
the three kinds of tests, benchmarks, and the most common ecosystem tools. Testing command-line programs end to end
(assert_cmd, trycmd) is covered on Testing CLIs; testing axum handlers on
Axum: Testing.
Unit tests
Unit tests live next to the code they test, in a module marked #[cfg(test)] so it is compiled only for cargo test.
use super::*; brings the parent module’s items — including private ones — into scope:
pub fn late_fee_cents(days_late: u32) -> u32 {
days_late.min(10) * 50
}
fn is_valid_isbn13(digits: &str) -> bool { // private: still testable from the child module
digits.len() == 13 && digits.bytes().all(|b| b.is_ascii_digit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fee_is_capped_at_ten_days() {
assert_eq!(late_fee_cents(3), 150);
assert_eq!(late_fee_cents(30), 500, "fee must be capped"); // optional message
}
#[test]
fn rejects_short_isbn() {
assert!(!is_valid_isbn13("978"));
assert_ne!(late_fee_cents(1), 0);
}
#[test]
#[should_panic(expected = "attempt to multiply with overflow")]
fn overflow_panics_in_debug() {
let _ = late_fee_cents(5) * u32::MAX; // only panics in builds with overflow checks
}
#[test]
fn parses_year() -> Result<(), std::num::ParseIntError> {
let year: u16 = "1965".parse()?; // `?` in tests: an Err fails the test
assert_eq!(year, 1965);
Ok(())
}
#[test]
#[ignore = "needs a running catalogue service"]
fn talks_to_remote_catalogue() {}
#[test]
fn lookup_matches_pattern() {
let r: Result<u16, String> = Ok(1965);
std::assert_matches!(r, Ok(1900..=1999)); // 1.96: prints the value on failure
}
}
Source: The Book: How to Write Tests
| Macro / attribute | Meaning |
|---|---|
|
fail if |
|
compare with |
|
fail unless the value matches the pattern (1.96) |
|
checked only when debug assertions are on (not in release builds) |
|
the test passes only if it panics with a message containing the text |
|
skipped unless run with |
a test returning |
|
Running tests: cargo test
cargo test # build and run unit, integration and doc tests
cargo test fee # only tests whose name contains "fee"
cargo test tests::parses_year -- --exact
cargo test -- --nocapture # show println! output of passing tests (alias: --show-output for passing ones)
cargo test -- --test-threads=1 # run sequentially (tests that share files or ports)
cargo test -- --ignored # run only ignored tests; --include-ignored runs all
cargo test --lib # only unit tests in the library
cargo test --test catalogue # only tests/catalogue.rs
cargo test --doc # only documentation tests
cargo test --release # optimised build (note: no overflow checks)
Arguments before -- go to Cargo; arguments after -- go to the test binary. Tests run in parallel threads in the same
process, so they must not depend on each other or on shared global state (environment variables, the current directory,
fixed file names or ports) unless they synchronise.
Integration tests
Files in a package’s tests/ directory are integration tests: each one is compiled as a separate crate that uses your
library like any external user — only its public API is reachable. Shared helpers go in tests/common/mod.rs (not
tests/common.rs, which would be treated as a test file itself).
shelf-core/
├── src/lib.rs
└── tests/
├── common/
│ └── mod.rs # shared fixtures: pub fn sample_catalog() -> InMemoryCatalog
└── lending.rs # an integration test crate
// tests/lending.rs
use shelf_core::{Author, Book, Catalog, CatalogError, InMemoryCatalog, Isbn};
fn sample_catalog() -> InMemoryCatalog {
let mut c = InMemoryCatalog::new();
let isbn = Isbn::parse("978-0-441-01359-3").unwrap();
c.add(Book::new(isbn, "Dune", vec![Author::new("Frank Herbert")], 1965)).unwrap();
c
}
#[test]
fn a_book_cannot_be_lent_twice() {
let mut c = sample_catalog();
let isbn = Isbn::parse("9780441013593").unwrap();
c.lend(&isbn, "alice").unwrap();
assert_eq!(c.lend(&isbn, "bob").unwrap_err(), CatalogError::AlreadyLent(isbn));
}
Source: The Book: Integration Tests
Binary crates (only main.rs) cannot be imported by integration tests — one more reason to put logic in lib.rs.
Documentation tests
Code blocks in /// doc comments are compiled and run as tests by cargo test, which keeps examples in documentation
correct. Since the 2024 edition, rustdoc compiles all doctests of a crate into a single binary where possible
(combined doctests), which makes cargo test --doc much faster; a doctest that must run on its own (for example because
it uses a #![feature] or depends on process-global state) is marked standalone_crate. Doctest attributes such as
no_run, should_panic, compile_fail and hidden # lines are covered on
Documentation with rustdoc.
Organising tests and testing private functions
The Book’s guidance: unit tests in #[cfg(test)] mod tests beside the code, integration tests in tests/, and the
public API covered by both. Rust lets unit tests call private functions (they are in a child module); whether you
should is a design choice — testing through the public API makes refactoring easier, while testing a tricky private
helper directly gives faster, more precise failures. Test-only helpers can be gated with #[cfg(test)], and test-only
dependencies go in [dev-dependencies].
Benchmarks with criterion
The built-in #[bench] attribute and test::Bencher are still nightly-only. On stable, use the criterion crate,
which runs each benchmark many times, removes outliers, reports confidence intervals and compares with the previous run.
Benchmarks live in benches/ and need harness = false:
[dev-dependencies]
criterion = "0.8"
[[bench]]
name = "isbn"
harness = false
Source: The Cargo Book: Benchmarks
// benches/isbn.rs
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
fn check_digit(digits: &[u8; 12]) -> u8 {
let sum: u32 = digits.iter().enumerate().map(|(i, &d)| u32::from(d) * if i % 2 == 0 { 1 } else { 3 }).sum();
((10 - sum % 10) % 10) as u8
}
fn bench_isbn(c: &mut Criterion) {
let prefix = [9, 7, 8, 0, 4, 4, 1, 0, 1, 3, 5, 9];
c.bench_function("isbn check digit", |b| b.iter(|| check_digit(black_box(&prefix))));
}
criterion_group!(benches, bench_isbn);
criterion_main!(benches);
Source: criterion 0.8 documentation
Run with cargo bench (or cargo bench — isbn to filter). std::hint::black_box stops the optimiser from computing
the result at compile time or discarding it. Mara & Holmes (Refactoring to Rust, chapter 6) benchmark Rust against
the code it replaces with criterion; compare both implementations in the same benchmark group so the report shows the
ratio. divan and iai-callgrind (instruction counts, stable in CI) are popular alternatives. Profiling is covered on
Performance & Profiling.
Testing Rust from other languages
When Rust replaces part of a Python, C or JavaScript code base, the existing test suite of the host language is the best
regression check: build the Rust code as an extension or library and run the old tests against it. Mara & Holmes
(chapter 7) test a PyO3 module with pytest; the same idea applies to a cdylib called from a C test harness or a
WebAssembly module tested from JavaScript. See Python
Interop with PyO3 and Incremental Adoption &
Refactoring.
Property testing with proptest
Property-based testing generates many random inputs and checks that a property holds for all of them; when one fails, proptest shrinks it to a minimal counter-example. It is excellent for parsers, encoders and anything with an inverse:
fn normalise(isbn: &str) -> String {
isbn.chars().filter(char::is_ascii_digit).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn normalising_is_idempotent(s in "[0-9-]{0,20}") {
let once = normalise(&s);
prop_assert_eq!(normalise(&once), once);
}
#[test]
fn keeps_every_digit(digits in proptest::collection::vec(0u8..10, 0..13)) {
let text: String = digits.iter().map(|d| format!("{d}-")).collect();
prop_assert_eq!(normalise(&text).len(), digits.len());
}
}
}
Source: proptest documentation
Snapshot testing with insta
Snapshot tests store the expected output of a function in a file the first time they run, then compare against it;
cargo insta review shows diffs interactively and accepts changes. They suit large or structured outputs: rendered
reports, JSON responses, error messages, CLI help text:
fn render_report(titles: &[&str]) -> String {
titles.iter().enumerate().map(|(i, t)| format!("{:>2}. {t}\n", i + 1)).collect()
}
#[cfg(test)]
mod tests {
#[test]
fn report_layout() {
insta::assert_snapshot!(super::render_report(&["Dune", "Emma"]), @r"
1. Dune
2. Emma
");
}
}
Source: insta documentation
The @"..." form keeps the snapshot inline in the source; without it, insta writes snapshots/*.snap files to commit
alongside the tests.
cargo-nextest and coverage
-
cargo-nextest (
cargo install cargo-nextest, thencargo nextest run) runs each test in its own process, in parallel, with better output, retries for flaky tests and JUnit reports for CI. It does not run doctests; keepcargo test --docfor those. -
cargo-llvm-cov uses the compiler’s LLVM source-based coverage:
cargo llvm-cov --html(or--lcovfor CI services) measures line and region coverage of tests, and works with nextest (cargo llvm-cov nextest).
cargo nextest run --workspace
cargo llvm-cov --workspace --lcov --output-path lcov.info
Source: cargo-nextest
Where the books are outdated
Mara & Holmes (Refactoring to Rust, chapter 6) use criterion 0.3; current criterion (0.8) keeps the same structure but
uses std::hint::black_box (stable since 1.66) rather than criterion::black_box. assert_matches! (1.96) and combined
doctests (2024) are newer than all the books.
References
-
Rust by Example: Testing (unit, doc, integration, dev-dependencies)
-
Lily Mara and Joel Holmes, Refactoring to Rust (Manning, 2025)