Rust Testing Beyond the Basics: Mocks, Property Tests and Benchmarks
You already know how to write a test with #[test] and assert_eq! (if not, start with Testing). That’s enough for small programs. But real projects soon raise new questions. Where should the tests live? How do you test code that sends emails or reads the clock? How do you find the one input you didn’t think of? And is your “faster” version really faster?
Think of it like a car workshop. Basic tests are checking that each part works. This page adds the rest of the workshop: a test bench for parts (fakes and mocks), a machine that shakes the car with thousands of random bumps (property tests), a photo of how things looked last time (snapshots), and a stopwatch you can trust (benchmarks).
One honest note before we start: the Rust Playground’s Run button runs main, not tests, and it doesn’t have the testing crates used below. So most examples here show real cargo test and cargo bench output from a small project on a normal computer. Where a program has a main, there’s a Try it button as usual.
Organising your tests
Section titled “Organising your tests”Here’s the layout of the little shop library used on this page:
shop/├── Cargo.toml├── src/│ ├── lib.rs <- code + unit tests in `mod tests`│ └── orders.rs├── tests/│ ├── checkout.rs <- an integration test│ └── common/│ └── mod.rs <- helpers shared by integration tests└── benches/ └── stock.rs <- a benchmarkRust has three kinds of tests, and cargo test runs all of them:
- Unit tests live next to the code, in a
#[cfg(test)] mod testsblock. They can see private functions too. - Integration tests live in the
tests/folder. Each file is compiled as a separate crate that uses your library from the outside, exactly like a user would. So they can only callpubitems. - Doc tests are the code examples inside
///comments. They prove your documentation still works. The Documentation page covers them in detail.
Here is part of src/lib.rs. Notice the doc test on apply_discount, and the helper and fixture functions inside mod tests:
/// One line in a shopping cart. Prices are in cents.#[derive(Debug, Clone, PartialEq)]pub struct Item { pub name: String, pub price_cents: u32, pub qty: u32,}
pub fn cart_total(items: &[Item]) -> u32 { items.iter().map(|i| i.price_cents * i.qty).sum()}
/// Takes `percent` off a price.////// ```/// use shop::apply_discount;////// assert_eq!(apply_discount(1000, 25), 750);/// ```pub fn apply_discount(price_cents: u32, percent: u32) -> u32 { assert!(percent <= 100, "discount must be at most 100%, got {percent}%"); price_cents - price_cents * percent / 100}
/// Turns "12.50" into 1250 cents.pub fn parse_price(text: &str) -> Result<u32, String> { /* ... */ }
/// Turns 1250 cents into "12.50".pub fn format_price(cents: u32) -> String { format!("{}.{:02}", cents / 100, cents % 100)}
#[cfg(test)]mod tests { use super::*;
// A helper: short to call, and every test builds items the same way. fn item(name: &str, price_cents: u32, qty: u32) -> Item { Item { name: name.to_string(), price_cents, qty } }
// A fixture: one realistic cart that several tests share. fn sample_cart() -> Vec<Item> { vec![item("coffee", 450, 2), item("bagel", 300, 1)] }
#[test] fn total_of_sample_cart() { assert_eq!(cart_total(&sample_cart()), 1200); }
#[test] fn empty_cart_costs_nothing() { assert_eq!(cart_total(&[]), 0); }
#[test] #[should_panic(expected = "at most 100%")] fn discount_over_100_panics() { apply_discount(1000, 150); }
// A test can return Result, so you can use `?` inside it. #[test] fn parses_a_price() -> Result<(), String> { let cents = parse_price("12.50")?; assert_eq!(cents, 1250); Ok(()) }
#[test] #[ignore = "slow: checks ten million prices"] fn every_price_round_trips() { for cents in 0..10_000_000 { assert_eq!(parse_price(&format_price(cents)), Ok(cents)); } }}A few things worth noticing:
- Helpers and fixtures are just functions. Rust has no special fixture system, and you rarely miss one. Because they sit inside
#[cfg(test)], they never end up in your real program. #[should_panic(expected = "...")]passes only if the panic message contains that text, so a panic from a different bug doesn’t count. Testing explains it with more examples.- Tests that return
Resultlet you use?. If the test returnsErr, it fails and prints the error, for exampleError: "missing '.' in \"12,50\"". #[ignore]skips a slow or special test by default. The text after=is the reason, andcargo testprints it.
The integration test uses the library through its public API. Its shared helper lives in tests/common/mod.rs:
use shop::Item;
pub fn breakfast_cart() -> Vec<Item> { vec![ Item { name: "coffee".into(), price_cents: 450, qty: 2 }, Item { name: "bagel".into(), price_cents: 300, qty: 1 }, ]}mod common;
use shop::{apply_discount, cart_total, format_price};
#[test]fn discounted_breakfast_total() { let total = cart_total(&common::breakfast_cart()); let to_pay = apply_discount(total, 10); println!("to pay: {}", format_price(to_pay)); assert_eq!(format_price(to_pay), "10.80");}Running cargo test shows one section per kind of test:
Running unittests src/lib.rs (target/debug/deps/shop-ffc1628aec947def)
running 5 teststest tests::every_price_round_trips ... ignored, slow: checks ten million pricestest tests::parses_a_price ... oktest tests::empty_cart_costs_nothing ... oktest tests::total_of_sample_cart ... oktest tests::discount_over_100_panics - should panic ... ok
test result: ok. 4 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.08s
Running tests/checkout.rs (target/debug/deps/checkout-c1e4aa0cf6375c2a)
running 1 testtest discounted_breakfast_total ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests shop
running 1 testtest src/lib.rs - apply_discount (line 16) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sChoosing which tests to run
Section titled “Choosing which tests to run”Day to day, you don’t always want every test. These commands help. Anything after a lone -- goes to the test program itself, not to Cargo.
cargo test discount # only tests whose name contains "discount"cargo test --test checkout # only the tests/checkout.rs filecargo test --lib # only the unit tests in src/cargo test --doc # only the doc testscargo test -- --ignored # only the #[ignore] testscargo test -- --include-ignored # everything, ignored ones toocargo test -- --nocapture # show println! output from passing testscargo test -- --test-threads=1 # run tests one at a timeFiltering matches part of the full test name, across all test files. Here discount finds one unit test and one integration test:
Running unittests src/lib.rs (target/debug/deps/shop-ffc1628aec947def)
running 1 testtest tests::discount_over_100_panics - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.09s
Running tests/checkout.rs (target/debug/deps/checkout-630939367802bf1e)
running 1 testtest discounted_breakfast_total ... okThe slow test runs only when you ask for it. Ten million round trips took about 3 seconds here:
running 1 testtest tests::every_price_round_trips ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 3.20sBy default, Rust hides println! output from tests that pass, so the report stays clean. --nocapture shows it:
$ cargo test --test checkout -- --nocapturerunning 1 testto pay: 10.80test discounted_breakfast_total ... okTest doubles: fake it with a trait
Section titled “Test doubles: fake it with a trait”Some code talks to the outside world: it sends emails, calls an API, or reads the clock. You don’t want your tests to send real emails. You also can’t test “what happens at 9 pm” if the code always asks the real clock.
The idiomatic Rust answer needs no library at all. Describe the dependency as a trait, make your function accept anything that implements it, and write a small fake for the tests. The real program passes the real thing. The tests pass the fake.
Here an order confirmation sends an email to the customer, and big orders also notify the sales team:
trait Mailer { fn send(&mut self, to: &str, body: &str);}
// The real mailer. In a real app this would call an email service.struct ConsoleMailer;
impl Mailer for ConsoleMailer { fn send(&mut self, to: &str, body: &str) { println!("email to {to}: {body}"); }}
struct Order { id: u32, email: String, total_cents: u32,}
fn confirm_order(order: &Order, mailer: &mut impl Mailer) { let body = format!("Order #{} confirmed", order.id); mailer.send(&order.email, &body); if order.total_cents >= 10_000 { }}
fn main() { let mut mailer = ConsoleMailer; confirm_order(&small, &mut mailer); confirm_order(&big, &mut mailer);}
#[cfg(test)]mod tests { use super::*;
// A fake: it doesn't send anything, it just writes down every call. #[derive(Default)] struct FakeMailer { sent: Vec<(String, String)>, }
impl Mailer for FakeMailer { fn send(&mut self, to: &str, body: &str) { self.sent.push((to.to_string(), body.to_string())); } }
#[test] fn small_order_sends_one_email() { let mut fake = FakeMailer::default();
confirm_order(&order, &mut fake);
assert_eq!(fake.sent.len(), 1); }
#[test] fn big_order_also_tells_sales() { let mut fake = FakeMailer::default();
confirm_order(&order, &mut fake);
assert_eq!(fake.sent.len(), 2); }}Output:
email to [email protected]: Order #7 confirmedemail to [email protected]: Order #8 confirmedemail to [email protected]: Big order #8This example has no external crates, so in the Playground you can also pick Test from the menu next to Run:
running 2 teststest tests::small_order_sends_one_email ... oktest tests::big_order_also_tells_sales ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sconfirm_order doesn’t know or care which mailer it gets. That’s the whole trick. The same idea works for a Clock trait (tests use a clock stuck at 9 pm), a PaymentGateway, or a Storage trait backed by a HashMap in tests. You’ll practise the clock version below.
Mocks with mockall
Section titled “Mocks with mockall”Writing fakes by hand is fine for one or two traits. When you have many, the mockall crate can generate them. Add it as a dev-dependency, so it’s only used for tests and examples (Cargo & Crates explains the sections of Cargo.toml):
cargo add --dev mockallPut #[automock] on the trait, and mockall creates a MockMailer type. The cfg_attr(test, ...) part means “only do this when testing”, so the mock never ends up in your real build. Cargo Features and #[cfg] explains cfg in more detail.
#[cfg(test)]use mockall::automock;
#[cfg_attr(test, automock)]pub trait Mailer { fn send(&mut self, to: &str, body: &str);}
// Order and confirm_order are the same as above.
#[cfg(test)]mod tests { use super::*;
#[test] fn big_order_also_tells_sales() { let mut mailer = MockMailer::new(); mailer .expect_send() .times(1) .return_const(()); mailer .expect_send() .times(1) .return_const(());
confirm_order(&order, &mut mailer); }}Each expect_send() is an expectation: “I expect send to be called with arguments that match withf, exactly once”. return_const(()) says what the mock returns (here, nothing). When the mock is dropped at the end of the test, mockall checks that every expectation was met.
To see it catch a bug, change the rule in confirm_order to order.total_cents > 12_000. Now the sales email is never sent:
thread 'orders::tests::big_order_also_tells_sales' (12345) panicked at src/orders.rs:4:18:MockMailer::send: Expectation(<function>) called 0 time(s) which is fewer than expected 1Fake or mock? Start with a hand-written fake. It’s plain Rust, easy to read, and has no extra dependency. Reach for mockall when you have many traits to fake, or when you really care about “called exactly N times with these arguments”.
Property-based testing with proptest
Section titled “Property-based testing with proptest”A normal test checks one example you picked: “12.50 parses to 1250”. A property test states a rule that should be true for every input, and then a library tries hundreds of random inputs to break it. It’s like hiring a very patient person to try every strange value you didn’t think of.
Good properties often look like this:
- Doing something twice undoes it: reversing a list twice gives the original list.
- A round trip gives back what you started with:
parse(format(x)) == x. - Some fact always holds: a sorted list has the same length as the input.
Add proptest with cargo add --dev proptest, and write tests inside the proptest! macro. Each name in strategy line says what kind of random values to generate:
use proptest::prelude::*;use shop::{format_price, parse_price};
proptest! { #[test] fn reversing_twice_gives_back_the_original(list in prop::collection::vec(any::<i32>(), 0..100)) { let mut copy = list.clone(); copy.reverse(); copy.reverse(); prop_assert_eq!(copy, list); }
#[test] fn price_survives_format_then_parse(cents in 0u32..10_000_000) { prop_assert_eq!(parse_price(&format_price(cents)), Ok(cents)); }}any::<i32>() means “any i32”, 0u32..10_000_000 means a number in that range, and vec(..., 0..100) builds lists of up to 99 items. By default each property runs 256 random cases. Use prop_assert_eq! instead of assert_eq! inside proptest!, so proptest can handle the failure nicely. Both pass:
running 2 teststest price_survives_format_then_parse ... oktest reversing_twice_gives_back_the_original ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01sNotice that this is the same check as the slow #[ignore] test from earlier. That test tried all ten million prices in 3 seconds. The property test tries 256 random ones in a hundredth of a second, so it can run every time.
Now let’s make a mistake on purpose. Someone “improves” format_price to print a thousands separator, like 1,000.00. It looks nicer, but they forget about parse_price:
pub fn format_price(cents: u32) -> String { let euros = cents / 100; let euros = if euros >= 1000 { format!("{},{:03}", euros / 1000, euros % 1000) } else { euros.to_string() }; format!("{euros}.{:02}", cents % 100)}Here’s the real output of cargo test --test properties, slightly trimmed:
---- price_survives_format_then_parse stdout ----proptest: Saving this and future failures in tests/properties.proptest-regressions
thread 'price_survives_format_then_parse' (12345) panicked at tests/properties.rs:4:1:Test failed: assertion failed: `(left == right)` left: `Err("bad euros in \"1,000.00\"")`, right: `Ok(100000)` at tests/properties.rs:15.minimal failing input: cents = 100000Look at minimal failing input: cents = 100000. The first random value that failed was probably something messy like 7,384,912. Proptest then shrinks it: it keeps trying smaller values that still fail, and reports the simplest one. 100000 cents is exactly 1,000.00, the first price that gets a comma. That points straight at the bug.
Proptest also saves the failing case in a .proptest-regressions file next to the test, and tries it first on every later run. Commit that file, so the bug can never quietly come back. If you prefer a smaller library, quickcheck does the same job with fewer features.
Snapshot tests with insta
Section titled “Snapshot tests with insta”Some output is long: a receipt, an error report, a big JSON document. Writing assert_eq! with the whole expected text is tiring. A snapshot test saves the output to a file the first time, and on later runs compares new output against that saved “photo”.
With insta (cargo add --dev insta), the test is one line:
pub fn receipt(items: &[Item]) -> String { let mut out = String::new(); for i in items { let line = format_price(i.price_cents * i.qty); out.push_str(&format!("{:<10} x{} {:>7}\n", i.name, i.qty, line)); } out.push_str(&format!("{:<14}{:>7}\n", "TOTAL", format_price(cart_total(items)))); out}
#[test]fn receipt_layout() { insta::assert_snapshot!(receipt(&sample_cart()));}The first run fails on purpose and writes a .snap.new file for you to review. After you accept it (with cargo insta review from the cargo-insta tool, or by renaming the file to .snap), it lives in src/snapshots/:
---source: src/lib.rsassertion_line: 84expression: receipt(&sample_cart())---coffee x2 9.00bagel x1 3.00TOTAL 12.00Later, if someone changes "TOTAL" to "Total", the test fails with a clear diff:
-old snapshot+new results────────────┬─────────────────────────────────────────────────────────────────── 1 1 │ coffee x2 9.00 2 2 │ bagel x1 3.00 3 │-TOTAL 12.00 3 │+Total 12.00────────────┴───────────────────────────────────────────────────────────────────To update snapshots run `cargo insta review` or set `INSTA_UPDATE=always`If the change was intended, you accept the new snapshot. If not, you just found a bug. Snapshots are great for “this shouldn’t change by accident” checks, but they don’t tell you the output is right. Read the first snapshot carefully before you accept it.
Benchmarks with criterion
Section titled “Benchmarks with criterion”When you want to know if code is fast, measure it. Guessing is wrong surprisingly often. Timing code inside a #[test] doesn’t work well either: tests are built without optimisations, and one run is too noisy.
criterion is the usual tool on stable Rust. It runs your code many times, does the statistics for you, and remembers the last result so it can tell you if you got faster or slower. Add it with cargo add --dev criterion, then tell Cargo about the benchmark file, and turn off the built-in harness:
[[bench]]name = "stock"harness = falseThis benchmark asks: to check if a product code is in stock, is a Vec or a HashSet faster for 1,000 codes?
use std::collections::HashSet;use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
fn stock_lookup(c: &mut Criterion) { // 1,000 product codes, like "SKU-0", "SKU-1", ... let codes: Vec<String> = (0..1000).map(|n| format!("SKU-{n}")).collect(); let code_set: HashSet<String> = codes.iter().cloned().collect(); let wanted = String::from("SKU-999"); // the worst case for a Vec: the last one
c.bench_function("vec contains", |b| { b.iter(|| codes.contains(black_box(&wanted))) }); c.bench_function("hashset contains", |b| { b.iter(|| code_set.contains(black_box(&wanted))) });}
criterion_group!(benches, stock_lookup);criterion_main!(benches);b.iter(...) is the code being measured. std::hint::black_box hides a value from the optimiser. Without it, the compiler might notice that the answer never changes, work it out once, and you’d be timing nothing.
Run it with cargo bench, which always builds in optimised (release) mode. Your numbers will be different on your computer:
vec contains time: [2.4222 µs 2.4470 µs 2.4742 µs]Found 2 outliers among 100 measurements (2.00%) 1 (1.00%) low mild 1 (1.00%) high mild
hashset contains time: [18.514 ns 18.891 ns 19.312 ns]Found 8 outliers among 100 measurements (8.00%) 4 (4.00%) high mild 4 (4.00%) high severeHow to read it: the three times are a range. The middle one is the best estimate, and the outer two say “the real value is very likely between these”. Here the HashSet takes about 19 nanoseconds and the Vec about 2.4 microseconds, so the set is over 100 times faster for this case. “Outliers” are a few unusually slow or fast runs, often caused by other programs on your computer. A few are normal.
Run it again after a change, and criterion compares with the last run:
vec contains time: [2.5124 µs 2.5430 µs 2.5773 µs] change: [−0.5279% +0.8368% +2.1133%] (p = 0.22 > 0.05) No change in performance detected.The change is under 1% and could be just noise, so criterion says “No change”. It also writes an HTML report in target/criterion/. For when (and when not) to care about speed, see Performance.
You may also see #[bench] functions in older code. They use the built-in test::Bencher, which only works on the nightly compiler:
#![feature(test)] // nightly onlyextern crate test;
#[bench]fn bench_total(b: &mut test::Bencher) { b.iter(|| (1..=1000u64).sum::<u64>());}On stable Rust, use criterion.
Coverage and CI
Section titled “Coverage and CI”Once you have tests, two more tools help. You install them with cargo install, like the other tools on Rust Tools & Setup.
cargo-nextest is a faster test runner. It runs each test in its own process, shows clearer output, and can retry flaky tests. One thing to know: it doesn’t run doc tests, so run cargo test --doc as well.
cargo install cargo-nextest --lockedcargo nextest runCoverage tells you which lines your tests actually ran. cargo-llvm-cov uses the coverage support built into the compiler and works on Linux, macOS and Windows:
cargo install cargo-llvm-covcargo llvm-cov # summary table in the terminalcargo llvm-cov --html # a browsable report in target/llvm-cov/htmlcargo-tarpaulin is an older, popular alternative. It works best on Linux and is a common choice in CI:
cargo install cargo-tarpaulincargo tarpaulin --out HtmlTreat coverage as a map, not a score. It shows code no test ever touches. It can’t tell you whether your asserts check the right thing. 80% coverage with good asserts beats 100% with weak ones.
In CI (GitHub Actions, GitLab and so on), a good starting point is the same three commands you run locally, so a failing check means the same thing everywhere:
cargo fmt --checkcargo clippy --all-targets -- -D warningscargo testCommon mistakes
Section titled “Common mistakes”Integration tests for a binary-only crate. If your project only has src/main.rs, files in tests/ can’t import anything from it:
error[E0432]: unresolved import `todo` --> tests/tasks.rs:1:5 |1 | use todo::add_task; | ^^^^ use of unresolved module or unlinked crate `todo`Fix: move the logic into src/lib.rs and keep main.rs small. main.rs then calls the library, and so do your tests. This also makes the code easier to reuse.
Putting shared helpers in tests/common.rs. Every .rs file directly inside tests/ is treated as its own test file, so you get a useless extra section in the output:
Running tests/common.rs (target/debug/deps/common-634ea0b624054b77)
running 0 testsFix: use tests/common/mod.rs, and mod common; in each test file that needs it.
Test crates in [dependencies]. proptest, mockall, insta and criterion belong under [dev-dependencies] (use cargo add --dev). Otherwise every user of your crate downloads and compiles them.
Tests that share state. Tests run in parallel. If two tests write the same file or change the same environment variable, they fail randomly. Give each test its own temporary folder (the tempfile crate helps), or as a last resort run with --test-threads=1.
Benchmarking without black_box, or in a test. The optimiser may delete work whose result is unused, and a debug-mode #[test] timing tells you little. Use criterion with black_box.
Practice
Section titled “Practice”1. A greeting you can test
Section titled “1. A greeting you can test”This function can’t be tested reliably, because it always asks the real clock:
fn greeting() -> &'static str { let hour = current_hour(); // reads the system time match hour { 5..=11 => "Good morning", 12..=17 => "Good afternoon", _ => "Good evening", }}Rewrite it to take a Clock trait with one method, fn hour(&self) -> u64. Write a FixedClock that always returns the same hour, and test the edges: 11 and 12, 17 and 18. Hint: struct FixedClock(u64); is enough. The real app would pass a SystemClock that reads the time.
Show solution
trait Clock { fn hour(&self) -> u64; // 0 to 23}
// A clock that always says the same hour. Perfect for tests.struct FixedClock(u64);
impl Clock for FixedClock { fn hour(&self) -> u64 { self.0 }}
fn greeting(clock: &impl Clock) -> &'static str { match clock.hour() { 5..=11 => "Good morning", 12..=17 => "Good afternoon", _ => "Good evening", }}
fn main() { for hour in [8, 12, 21] { println!("{hour}:00 -> {}", greeting(&FixedClock(hour))); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn morning_ends_at_noon() { assert_eq!(greeting(&FixedClock(11)), "Good morning"); assert_eq!(greeting(&FixedClock(12)), "Good afternoon"); }
#[test] fn evening_starts_at_six() { assert_eq!(greeting(&FixedClock(17)), "Good afternoon"); assert_eq!(greeting(&FixedClock(18)), "Good evening"); }
#[test] fn very_early_is_still_evening() { assert_eq!(greeting(&FixedClock(3)), "Good evening"); }}Output:
8:00 -> Good morning12:00 -> Good afternoon21:00 -> Good eveninggreeting no longer decides where the time comes from, its caller does. That one change makes every hour testable in milliseconds. Pick Test in the Playground menu and all 3 tests pass.
2. Find the rule, then let proptest check it
Section titled “2. Find the rule, then let proptest check it”Here is a function that turns a blog tag like " Web Dev " into "web-dev":
fn normalize_tag(tag: &str) -> String { tag.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-")}Write two property tests with proptest: normalizing a tag that is already normalized changes nothing, and the result never contains whitespace. Hint: the strategy ".*" generates random strings (it’s a regular expression). For “no whitespace”, prop_assert!(!s.contains(char::is_whitespace)) works.
Show solution
fn normalize_tag(tag: &str) -> String { tag.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-")}
fn main() { for tag in ["Rust", " Web Dev ", "GAME dev"] { println!("{tag:?} -> {:?}", normalize_tag(tag)); }}
#[cfg(test)]mod tests { use super::*; use proptest::prelude::*;
proptest! { #[test] fn normalizing_twice_changes_nothing(tag in ".*") { let once = normalize_tag(&tag); prop_assert_eq!(normalize_tag(&once), once); }
#[test] fn result_has_no_whitespace(tag in ".*") { let normalized = normalize_tag(&tag); prop_assert!(!normalized.contains(char::is_whitespace)); } }}Output:
"Rust" -> "rust"" Web Dev " -> "web-dev""GAME dev" -> "game-dev"Run works in the Playground, but Test doesn’t, because the Playground has no proptest. In your own project, add it with cargo add --dev proptest and run cargo test:
running 2 teststest tests::result_has_no_whitespace ... oktest tests::normalizing_twice_changes_nothing ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02sThe random strings include tabs, newlines and non-English letters, which you’d probably never type into a hand-written test. “Doing it twice is the same as doing it once” is a property worth checking for any clean-up function.
- Unit tests go in
#[cfg(test)] mod testsnext to the code, integration tests intests/(shared helpers intests/common/mod.rs), and doc tests in///comments. - Use
#[ignore]for slow tests, run them withcargo test -- --ignored, filter by name withcargo test some_name, and seeprintln!output with-- --nocapture. - To test code that talks to the outside world, put that dependency behind a trait and pass a fake in tests. Use mockall when hand-written fakes get tedious.
- Property tests (proptest) check a rule against hundreds of random inputs and shrink a failure to the simplest example. Snapshot tests (insta) catch accidental changes in big outputs.
- Measure speed with criterion and
cargo bench, and wrap inputs instd::hint::black_box.#[bench]needs nightly. - cargo-nextest runs tests faster; cargo-llvm-cov and cargo-tarpaulin show which lines your tests cover.
Quick check
Section titled “Quick check”Quick check
1. Several files in tests/ need the same helper function. Where should it go?
2. What does cargo test -- --ignored do?
3. A proptest property fails. What does proptest report as the minimal failing input?
4. Why do criterion benchmarks wrap values in std::hint::black_box?
Score: 0 / 4
Read more
Section titled “Read more”- Writing Automated Tests — The Rust Book: the official chapter on writing, running and organising unit and integration tests.
- Testing — Rust by Example: short examples of unit tests, doc tests, integration tests and dev-dependencies.
- The proptest book: strategies, shrinking, configuration and how proptest compares to quickcheck.
- mockall documentation:
#[automock], expectations, matching arguments, return values and mocking structs. - Criterion.rs user guide: getting started, reading the output, comparing functions and the HTML reports.