Unit testing in Rust: #[test], assert_eq! and cargo test
How to write tests in Rust: #[test] functions, assert!, assert_eq! with messages, should_panic, tests returning Result, and unit vs integration tests.
Rust has testing built in: no framework to install. A test is a function marked #[test]; cargo test compiles your crate in test mode, runs every test, and reports which passed.
Unit tests live next to the code
pub fn apply_discount(price_cents: u64, percent: u64) -> u64 {
price_cents - price_cents * percent / 100
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ten_percent_off() {
assert_eq!(apply_discount(1000, 10), 900);
}
#[test]
fn zero_percent_changes_nothing() {
assert_eq!(apply_discount(1999, 0), 1999, "0% should keep the price");
}
}
#[cfg(test)]compiles the module only forcargo test, so tests add nothing to your real program.use super::*;brings the parent module's items into scope, including private functions: unit tests can test internals.
$ cargo test
running 2 tests
test tests::ten_percent_off ... ok
test tests::zero_percent_changes_nothing ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
cargo test discount runs only tests whose name contains "discount". Output from println! in passing tests is hidden; cargo test -- --nocapture shows it.
The assertion macros
| Macro | Fails when |
|---|---|
assert!(cond) |
cond is false |
assert_eq!(left, right) |
they're not equal; prints both values |
assert_ne!(left, right) |
they're equal |
All accept a trailing message with format arguments: assert_eq!(total, 30, "cart total for {items:?}"). The values compared with assert_eq! must implement PartialEq and Debug, so derive both on your types. Floats rarely compare exactly; assert that the difference is small: assert!((a - b).abs() < 1e-9).
Testing failures
#[should_panic(expected = "divide by zero")]passes only if the test panics with a message containing that text.- A test can return
Result<(), E>and use?inside; returningErrfails the test. - For functions returning
Result, assert on the error:assert!(parse("x").is_err())orassert_eq!(parse("x"), Err(ParseError::Empty)).
Integration tests and doc tests
- Files in a top-level
tests/directory are integration tests: each one is a separate crate that can only use your library's public API, the way real users would. - Code blocks in
///doc comments are compiled and run as doc tests bycargo test, so examples in your documentation can't go stale.
Good tests at work
- Name tests as sentences describing behavior:
rejects_empty_username, nottest1. - One behavior per test, with the edge cases: empty input, zero, negative numbers, unicode, the maximum value.
- Test through the public interface where possible; tests that poke at internals break on every refactor.
- Keep tests deterministic: no real clock, no network, no random numbers without a fixed seed.
- Run
cargo testin CI on every pull request, alongsidecargo clippyandcargo fmt --check.
Common mistakes
- Forgetting
use super::*;: "cannot find functionapply_discountin this scope". - Comparing types without
PartialEqorDebuginassert_eq!(E0369 or E0277): derive them. #[should_panic]withoutexpected: the test passes for any panic, including the wrong one.
Example
pub fn apply_discount(price_cents: u64, percent: u64) -> u64 {
assert!(percent <= 100, "percent must be at most 100, got {percent}");
price_cents - price_cents * percent / 100
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
Empty,
NotANumber(String),
}
pub fn parse_qty(s: &str) -> Result<u32, ParseError> {
let s = s.trim();
if s.is_empty() {
return Err(ParseError::Empty);
}
s.parse().map_err(|_| ParseError::NotANumber(s.to_string()))
}
fn main() {
println!("{}", apply_discount(1000, 10));
println!("{:?}", parse_qty(" 7 "));
println!("{:?}", parse_qty(""));
println!("{:?}", parse_qty("seven"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ten_percent_off() {
assert_eq!(apply_discount(1000, 10), 900);
}
#[test]
fn full_discount_is_free() {
assert_eq!(apply_discount(1999, 100), 0, "100% off should cost nothing");
}
#[test]
#[should_panic(expected = "at most 100")]
fn rejects_discount_over_100() {
apply_discount(1000, 150);
}
#[test]
fn parses_with_whitespace() -> Result<(), ParseError> {
assert_eq!(parse_qty(" 42\n")?, 42);
Ok(())
}
#[test]
fn reports_empty_and_bad_input() {
assert_eq!(parse_qty(" "), Err(ParseError::Empty));
assert_eq!(parse_qty("4x"), Err(ParseError::NotANumber("4x".to_string())));
}
}
Output:
900
Ok(7)
Err(Empty)
Err(NotANumber("seven"))