Rust Test Organisation
Test organisation is the set of places where the three kinds of test live. Unit tests, integration tests, and doc tests each live in a different place.
Three homes at a glance
The same function can be guarded from three places. The place decides what the test may touch:
// unit test in src/, inside a #[cfg(test)] module
// integration test in tests/, as its own crate
// doc test in a /// example, above the functionName the behaviour, not the function
A good name says what must hold. It names the case and the outcome:
#[test]
fn empty_basket_totals_zero() {
assert_eq!(Basket::new().total(), 0);
}
#[test]
fn discount_applies_over_1000() {
assert_eq!(apply_discount(2000, 10), 1800);
}Group tests inside a module
A file with many tests grows hard to scan. Inner modules group the tests by topic. The inner test answers to the full name tests::discount::ten_percent_off:
#[cfg(test)]
mod tests {
use super::*;
mod discount {
use super::*;
#[test]
fn ten_percent_off() {
assert_eq!(apply_discount(2000, 10), 1800);
}
}
}Build fresh state in a helper
Two tests need the same basket. A small helper builds a new one for each test:
fn basket_with_books() -> Basket {
let mut basket = Basket::new();
basket.add(1200);
basket.add(800);
basket
}The docs carry a test too
This example sits in the doc comment of add_prices. Cargo lifts it out. It runs on every cargo test:
/// Adds the prices of two items, in cents.
///
/// ```
/// let total = shop::add_prices(300, 250);
/// assert_eq!(total, 550);
/// ```Example
fn reading_level(age: i32) -> &'static str {
if age < 6 {
"starter"
} else if age < 13 {
"middle"
} else {
"young adult"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starter_for_a_five_year_old() {
assert_eq!(reading_level(5), "starter");
}
#[test]
fn middle_for_a_nine_year_old() {
assert_eq!(reading_level(9), "middle");
}
}
fn main() {
println!("age 5 = {}", reading_level(5));
println!("age 9 = {}", reading_level(9));
println!("age 15 = {}", reading_level(15));
}Key points
- Unit test: lives in a #[cfg(test)] module beside the code.
- It can call private items.
- Integration test: lives in the tests/ folder.
- It can call public items only.
- Doc test: lives in a /// code example.
- It teaches the reader.
- Cargo runs it as a test too.
- Name each test for the behaviour it guards.
- A failing test name should read like a bug report.
- Give every test its own fresh state.
- Tests run in parallel.
- Shared state makes failures appear and vanish.
- Many small files slow cargo test down.
- One file with modules inside is often the faster shape.
In this example, reading_level sorts ages into three bands. Under 6 is starter. Under 13 is middle. The rest is young adult. Two unit tests guard the first two bands. They sit in the module at the bottom of the file. The same function also carries a doc example, like the one in the snippet above. That example is a third test. It guards the middle band. main prints all three bands, so you can see the answers the tests check.
Note
Every file in tests/ builds as its own crate.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
A test must call a private helper. Where does that test belong?