20. Unit Tests, Integration Tests, doctests
Rust ships its test runner inside cargo. Add `#[test]` to a function and `cargo test` finds and runs it. This lesson covers unit tests (in a `mod tests` block), integration tests (under `tests/`), and doctests that automatically run code blocks in /// comments.
What you'll learn
- 1Write `#[test]` functions and run them with `cargo test`
- 2Follow the `mod tests` convention for unit tests
- 3Write integration tests under `tests/`
- 4Use `#[should_panic]` to verify a function panics
- 5Run code in /// comments as doctests
Overview
Having tests built into cargo is a big deal β no "which test framework should I use" decisions, and every new project can adopt testing from day one.
Core Concepts
1) Unit tests in the same file
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_works() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn add_negative() {
assert_eq!(add(-1, 1), 0);
}
}- `#[cfg(test)]` β only compiled in test builds
- `use super::*` β imports private items from the parent module
- Only `#[test]`-marked fns get executed by cargo test
2) Integration tests in tests/
Test the public API only β same paths a downstream user would use.
// tests/api.rs
use my_crate::add;
#[test]
fn integration_add() {
assert_eq!(add(10, 20), 30);
}3) Common assertion macros
| Macro | Use |
|---|---|
| assert!(expr) | expr must be true |
| assert_eq!(a, b) | a == b |
| assert_ne!(a, b) | a != b |
| #[should_panic] | test must panic to pass |
| #[ignore] | skip by default (cargo test -- --ignored) |
4) Doctests β run code inside /// comments
/// Add two numbers.
///
/// # Examples
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }Hands-on Examples
Result-returning test and should_panic:
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 { panic!("div by zero"); }
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ok_case() -> Result<(), String> {
if divide(10, 2) == 5 { Ok(()) } else { Err("expected 5".into()) }
}
#[test]
#[should_panic(expected = "div by zero")]
fn panic_case() { divide(10, 0); }
}Useful invocations:
cargo test # everything
cargo test add_ # name contains add_
cargo test -- --nocapture # show println! output
cargo test --doc # doctests only
cargo test -- --ignored # run ignored onesCommon Mistakes
Q. println! doesn't show up in cargo test
A. By design β passing tests hide output. Use `cargo test -- --nocapture` to see it.
Q. mod tests vs. tests/?
A. **mod tests** = same crate, has access to private items. **tests/** = external view, public API only. Use both as needed.
Q. Tests run too slow
A. cargo test parallelizes by default. Force serial with `cargo test -- --test-threads=1`. Or mark slow tests `#[ignore]` and run on demand.
Recap
- cargo test auto-discovers #[test] functions
- mod tests + #[cfg(test)] is the unit-test convention
- tests/ folder is for integration tests (public API)
- Code blocks in /// comments run as doctests
- Useful: should_panic / ignore / assert_eq
Try It Yourself
- Add 3+ unit tests in a `mod tests` block for an existing function
- Add one integration test under `tests/api.rs`
- Embed a doctest in /// comments and verify `cargo test --doc` passes
All lecture materials and example code are openly available on GitHub.
View on GitHub β