Testing#
Rust ships its own test runner. Any function annotated with
#[test] becomes a test; cargo test discovers, builds,
and runs them. Integration tests live under tests/; doc
tests live inside /// comments; bench tests use criterion
(nightly built-in benches are still experimental); property tests
use proptest or quickcheck; fuzzers use cargo-fuzz
or afl.
For
assert! family in production, see Errors.
Unit tests#
Drop a #[cfg(test)] module inside the source
file; cargo test compiles it only for test builds.
pub fn trim(s: &str) -> &str { s.trim() }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trim_removes_whitespace() {
assert_eq!(trim(" hi "), "hi");
}
#[test]
#[should_panic(expected = "divide by zero")]
fn divide_by_zero_panics() {
divide(10, 0);
}
}
$ cargo test
running 2 tests
test tests::trim_removes_whitespace ... ok
test tests::divide_by_zero_panics ... ok
Integration tests#
Each file under tests/ is a separate crate that exercises
the library through its public API.
my-crate/
src/lib.rs
tests/
integration.rs
// tests/integration.rs
use my_crate::Server;
#[test]
fn handles_ping() {
let s = Server::new();
assert_eq!(s.ping(), "pong");
}
Integration tests cannot access pub(crate) items; they see
only the public API.
Doc tests#
Code blocks inside /// doc comments are compiled and run.
Go-tos for keeping examples honest.
/// Returns the maximum of two values.
///
/// ```
/// assert_eq!(my_crate::max(2, 3), 3);
/// assert_eq!(my_crate::max(5, 1), 5);
/// ```
pub fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
cargo test --doc runs only doc tests.
Assertions#
The standard set.
Macro |
Meaning |
|---|---|
|
panic if |
|
|
|
|
|
(nightly / |
|
explicit panic |
|
signal an impossible path |
|
this test must panic to pass |
Table-driven#
For functions with a small input space, write a single test with a table of cases.
#[test]
fn trim_table() {
let cases = [
("clean", "hi", "hi"),
("trailing", "hi ", "hi"),
("leading", " hi", "hi"),
("both", " hi ", "hi"),
("empty", "", ""),
("whitespace", " ", ""),
];
for (name, input, want) in cases {
assert_eq!(trim(input), want, "case {name}");
}
}
Helper fixtures#
Common setup goes into a regular function; the operator calls
it from each test. cargo test runs tests in parallel by
default (--test-threads=1 forces serial; useful when tests
share global state).
fn fresh_server() -> Server {
let s = Server::start(":0").expect("start");
wait_for_ready(&s);
s
}
#[test]
fn handles_ping() {
let s = fresh_server();
assert_eq!(s.ping(), "pong");
}
Mocking#
Rust’s stdlib has no mock framework; the operator either parameterises behaviour through a trait (and uses a fake impl in tests) or reaches for mockall.
trait Clock { fn now(&self) -> u64; }
struct SystemClock;
impl Clock for SystemClock { fn now(&self) -> u64 { /* … */ 0 } }
#[cfg(test)]
struct FixedClock(u64);
#[cfg(test)]
impl Clock for FixedClock { fn now(&self) -> u64 { self.0 } }
#[cfg(test)]
#[test]
fn uses_clock() {
let c = FixedClock(1234);
assert_eq!(timestamp(&c), 1234);
}
proptest (property-based)#
proptest generates random inputs against a property.
[dev-dependencies]
proptest = "1"
use proptest::prelude::*;
proptest! {
#[test]
fn rev_twice_is_identity(s in "\\PC*") {
let twice: String = rev(&rev(&s));
prop_assert_eq!(s, twice);
}
}
Run property tests on parsers, codecs, and any function whose contract can be expressed without enumerating inputs.
cargo-fuzz#
LLVM libFuzzer driver. Catches panics, OOM, and sanitizer violations.
$ cargo install cargo-fuzz
$ cargo fuzz init
$ cargo fuzz add parse_target
// fuzz_targets/parse_target.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let _ = my_crate::parse(data); // must not panic
});
$ cargo fuzz run parse_target -- -max_total_time=300
Benchmarks (criterion)#
criterion runs statistical benchmarks on stable Rust.
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "parse"
harness = false
// benches/parse.rs
use criterion::{Criterion, criterion_group, criterion_main};
fn bench(c: &mut Criterion) {
let data = std::fs::read("samples/big.bin").unwrap();
c.bench_function("parse", |b| b.iter(|| my_crate::parse(&data)));
}
criterion_group!(benches, bench);
criterion_main!(benches);
$ cargo bench
Coverage#
Use cargo-llvm-cov (LLVM-based) or cargo-tarpaulin.
$ cargo install cargo-llvm-cov
$ cargo llvm-cov --html
$ open target/llvm-cov/html/index.html