Testing is one of Rust's proudest features. Even though the compiler checks the code strictly, it cannot know the correctness of business logic. Rust supports three types of tests as a standard: unit tests, integration tests, and documentation tests, and all can be run with a single line in cargo test.
Learning Objectives
- Write unit tests with the
#[test]function inside#[cfg(test)] mod tests. - Place integration tests in the
tests/directory and validate the library as an external user would. - Understand that code blocks in
///documentation comments are executed as doctests. - Know frequently used macros such as
assert!,assert_eq!,assert_ne!,should_panic, etc.
Key Concepts
1) Unit Tests — Inside the Same File
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_positive() {
assert_eq!(add(2, 3), 5);
}
}
mod testswith#[cfg(test)]is compiled only duringcargo test.- Can access private items in the same file (parent module) using
use super::*;— the strength of unit tests.
2) Integration Tests — tests/ Directory
hello/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
Each .rs file in tests/ is compiled like an independent crate. Only the library's public API is available, allowing validation from the perspective of an external user.
3) Doctest — /// Code Blocks
/// # Example
/// ```
/// use mycrate::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
The code inside the document comments ``` is automatically executed with doctest. The documentation and tests are created at the same time. (However, it only works in the public API of a library crate.)
4) Frequently Used Macros
| Macro | Meaning |
|---|---|
assert!(expr) |
Panics if not true |
assert_eq!(a, b) |
Panics if a == b is not true (prints both values) |
assert_ne!(a, b) |
Panics if a != b is not true |
#[should_panic] |
Passes only if the body panics |
#[ignore] |
Usually skipped and only run with cargo test -- --ignored |
5) cargo test Command
cargo test #All tests
cargo test add_positive #Only those with 'add_positive' in the name
cargo test --lib #Unit tests only
cargo test --test integration_test #Only specific files in tests/
cargo test --doc #Doctest only
cargo test -- --nocapture #Print even println! inside the tests
Examples
Example 1 — src/lib.rs : Unit Test and Doctest
//Episode 20: Library code + unit tests + documentation tests (doctest)
/// Returns the sum of two integers.
///
/// # Example
/// ```
/// use testing::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Checks whether an integer is even.
///
/// # Example
/// ```
/// use testing::is_even;
/// assert!(is_even(4));
/// assert!(!is_even(7));
/// ```
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}
//It is Rust's convention to place the unit test module in the same file.
//Thanks to `#[cfg(test)]`, it is only compiled when running `cargo test`.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_positive() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn add_zero() {
assert_eq!(add(0, 0), 0);
}
#[test]
fn even_true() {
assert!(is_even(4));
assert!(is_even(0));
}
#[test]
fn even_false() {
assert!(!is_even(7));
}
}
Example 2 — tests/integration_test.rs : Integration Test
//Files in the `tests/` folder are treated as integration tests.
//They are verified using only the public API of the library, just like an external user would.
use testing::{add, is_even};
#[test]
fn integration_add() {
assert_eq!(add(10, 20), 30);
assert_eq!(add(-5, 5), 0);
}
#[test]
fn integration_even() {
for n in [0, 2, 4, 100] {
assert!(is_even(n));
}
for n in [1, 3, 5] {
assert!(!is_even(n));
}
}
Example 3 — src/main.rs : Using a Library
//A small executable file that uses the API of the library (`testing`).
//Run tests with `cargo test`.
use testing::{add, is_even};
fn main() {
println!("add(2, 3) = {}", add(2, 3));
println!("is_even(4) = {}", is_even(4));
println!();
println!("Run the tests of this crate with the following commands:");
println!("cargo test # All tests");
println!("cargo test --lib # Unit tests only");
println!("cargo test --test integration_test # Integration tests only");
println!("cargo test --doc # Doctests only");
}
Common Mistakes
Q. cargo test finds nothing.
A. Check if #[test] is missing in the test function or if there are functions outside #[cfg(test)] mod tests. Integration tests must be inside the tests/ directory.
Q. Doctest does not run.
A. Doctest only works in the public API of a library crate. Crates that are binary-only (having only src/main.rs) ignore doctest. You need src/lib.rs, like in this example.
Q. How can I provide richer messages when assert_eq! fails?
A. It accepts additional arguments.
assert_eq!(add(2, 3), 5, "The sum of two positive numbers");
Q. How do I test a function that is supposed to panic?
A. Attach the #[should_panic] or #[should_panic(expected = "part of the message")] attribute. If the body does not panic, the test is considered a failure.
Summary
- Unit tests are in the same file with
#[cfg(test)] mod tests— can access private items. - Integration tests are in the
tests/directory — use only public APIs and verify from an external perspective. - Documentation tests are in the
///```blocks — combine documentation and verification in one place. - All tests run with
cargo testin a single line — can run partially with filters/flags.
Try it yourself
Solve the assignment below. The reference solution is shown below.