Why this matters
So far you have checked programs by running them and looking at the output. That stops scaling quickly: every change means re-checking everything by hand, and the edge case you checked last week is forgotten. Automated tests are small functions that call your code and assert what the result must be. They run in seconds, catch regressions the moment they happen, and document the intended behavior better than comments do. Rust has testing built into the compiler and cargo, with no framework to install.
A first test
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
println!("{}", add(2, 2));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_small_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn adds_negatives() {
assert_eq!(add(-2, -3), -5);
}
}
#[test]marks a function as a test: no arguments, no return value (or aResult, below).- Tests conventionally live in a module named
testsat the bottom of the file, marked#[cfg(test)]so it is only compiled when testing. use super::*;imports everything from the parent module (the code under test), including private functions.
Running it: with plain rustc, compile in test mode and run the result. (With cargo, cargo test does both.)
$ rustc --edition 2021 --test -o t main.rs
$ ./t
running 2 tests
test tests::adds_negatives ... ok
test tests::adds_small_numbers ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
--test builds a different program: instead of your main, a generated test runner that calls every #[test] function (in parallel threads), catches panics, and reports. A test passes if it returns without panicking and fails if it panics.
Assertions
assert!(condition); // panics if false
assert!(condition, "message {}", x); // with a custom message
assert_eq!(left, right); // panics if left != right, printing both
assert_ne!(left, right); // panics if they are equal
assert_eq! needs the values to implement PartialEq (to compare) and Debug (to print them on failure). When a test fails, the output shows exactly what went wrong:
fn average(v: &[i32]) -> i32 {
v.iter().sum::<i32>() / v.len() as i32
}
fn main() {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn average_of_three() {
assert_eq!(average(&[1, 2, 4]), 2);
}
#[test]
fn average_rounds() {
assert_eq!(average(&[1, 2]), 2, "1 and 2 should round up to 2");
}
}
$ ./t
running 2 tests
test tests::average_of_three ... ok
test tests::average_rounds ... FAILED
failures:
---- tests::average_rounds stdout ----
thread 'tests::average_rounds' panicked at main.rs:18:9:
assertion `left == right` failed: 1 and 2 should round up to 2
left: 1
right: 2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::average_rounds
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The runner exits with a non-zero status when any test fails, which is what CI systems check. (Here the test documents a requirement the implementation does not meet: integer division truncates. Whether the test or the code is wrong is a design question; the test made it visible.)
Running a subset
The test binary accepts a filter: ./t average runs only tests whose names contain "average"; cargo test average does the same. ./t --nocapture shows println! output from passing tests (normally captured and shown only for failures). #[ignore] marks slow tests to skip unless asked with --ignored.
Testing private functions
Because tests is a child module, use super::* gives it access to private items. Rust's convention is that unit tests sit next to the code and may test internals; integration tests (in a separate tests/ directory of a cargo project) use only the public API, like an outside user would.
What a good unit test looks like
- One behavior per test, with a name that says what it checks:
century_is_not_leap, nottest3. - Arrange, act, assert: set up inputs, call the function, assert on the result.
- Edge cases: empty input, zero, one element, the maximum value, negative numbers, boundaries on both sides.
- Deterministic: no dependence on time, randomness, network, or the order tests run in.
- Fast: hundreds of tests should run in a second, so you run them constantly.
Common mistakes
- Forgetting
use super::*;: E0425 "cannot find functionaddin this scope" inside the test module. - Missing
#[test]: the function compiles but never runs; the count in "running N tests" gives it away. assert_eq!on a type withoutDebug: E0277; deriveDebug(andPartialEq).- Floats with
assert_eq!:0.1 + 0.2is not0.3. Assert(a - b).abs() < 1e-9. - Tests that pass for the wrong reason: a test should fail when the code is wrong. Briefly break the code and check that the test catches it, which is exactly what the first exercise's checker does.
Recap
#[test]functions in a#[cfg(test)] mod testswithuse super::*;are unit tests.rustc --test(orcargo test) builds a test runner; a test passes unless it panics.assert!,assert_eq!,assert_ne!(with optional messages) express expectations and print values on failure.- Filter tests by name;
--nocaptureshows output;#[ignore]skips slow tests. - Good tests are small, named for one behavior, cover edge cases, and fail when the code is broken.
Going further: in the sandbox shell, create a cargo project, put a function and tests in src/lib.rs, and run cargo test. Read the Rust Book chapter 11.1, "How to Write Tests".