Testing
Write unit tests, integration tests, and doc tests in Rust — and run them efficiently with cargo test.
- Write unit tests with
- Use assert_eq!, assert_ne!, and assert! macros correctly
- Write integration tests in the tests/ directory
- Add runnable code examples as doc tests
- Use cargo test flags to filter and control test runs
Rust's test framework is built into the language and toolchain — no external library needed. Tests live alongside source code, are compiled only when needed, and cargo test runs them all. The result is a culture where testing is the default rather than an afterthought.
Unit tests
Unit tests live in the same file as the code they test, inside a #[cfg(test)] module. This module is compiled only when running tests, keeping it out of production binaries:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*; // import everything from the parent module
#[test]
fn test_add_basic() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
assert_eq!(add(-5, -3), -8);
}
}#[test] marks a function as a test case. assert_eq!(left, right) panics with a helpful message if the values aren't equal. A panicking test function counts as a failure.
Assertion macros
| Macro | Passes when |
|---|---|
assert!(expr) | expr is true |
assert_eq!(a, b) | a == b |
assert_ne!(a, b) | a != b |
All three accept an optional format string for custom failure messages:
assert_eq!(result, expected, "expected {} for input {:?}", expected, input);assert_eq! requires the values to implement both Debug and PartialEq. When a test fails, it prints both values — which is far more useful than a plain assert!(a == b) that only says "assertion failed."
Testing panics
Use #[should_panic] to assert that a function panics:
#[test]
#[should_panic(expected = "divide by zero")]
fn test_divide_by_zero() {
let _ = 10 / 0;
}The expected attribute checks that the panic message contains the given substring. This makes the test more precise — it would fail if the function panics with a different message.
Returning Result from tests
Tests can return Result<(), E> — allowing ? to be used inside them:
#[test]
fn test_parse() -> Result<(), std::num::ParseIntError> {
let n: i32 = "42".parse()?;
assert_eq!(n, 42);
Ok(())
}If the function returns Err, the test fails with the error's Debug output.
Integration tests
Integration tests live in a tests/ directory at the crate root. Each file is compiled as a separate crate that depends on your library:
src/
lib.rs
tests/
integration_test.rs// tests/integration_test.rs
use my_crate::add; // must use the public API
#[test]
fn test_add_via_public_api() {
assert_eq!(add(10, 20), 30);
}Integration tests can only access your public API — they test from a user's perspective. This is a useful discipline: if something is hard to test from the outside, its interface may need redesign.
Integration tests only work for library crates (lib.rs). For binary crates (main.rs), extract logic into a library and test the library. This forces a clean separation between the binary entry point and the testable logic.
Doc tests
Code examples in doc comments are run as tests by default:
/// Adds two numbers.
///
/// # Examples
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}cargo test --docDoc tests verify that your documentation examples actually work. This is one of Rust's most distinctive testing features — documentation and tests are automatically kept in sync.
cargo test flags
cargo test # run all tests
cargo test test_add # run tests whose names contain "test_add"
cargo test -- --nocapture # show println! output even when tests pass
cargo test -- --test-threads=1 # run tests sequentially (no parallelism)
cargo test --doc # run only doc tests
cargo test --test integration_test # run a specific integration test fileTests run in parallel by default. Use --test-threads=1 when tests share mutable global state (or restructure so they don't).
Check your understanding
Knowledge check
- 1.Why is the test module marked with #[cfg(test)]?
- 2.What can integration tests in tests/ access?
- 3.Code examples in doc comments (///) are compiled and run automatically by cargo test.
Do it yourself
Add tests to a small function you've written:
- A unit test that verifies the happy path.
- A test with
#[should_panic]for an invalid input. - An integration test in
tests/that exercises the public API end-to-end. - A doc test example in the function's
///comment.
Run cargo test and verify all four pass. Then deliberately break one and observe the failure output.
Where to go next
Tests prove your code is correct today. Macros help eliminate the boilerplate you'd have to test again and again. Next: an introduction to declarative macros with macro_rules!.
Cargo in depth
Manage multi-crate workspaces, conditional compilation with features, build scripts, and publishing crates to crates.io.
Macros: an introduction
Write code that writes code with declarative macro_rules! macros — understand the pattern matching syntax, common use cases, and when to reach for proc macros instead.