← Back to Rust series
πŸ¦€
Modern Rust
Modern Β· Prerequisite: lesson 19

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.

Rusttestcargo testdoctestassert_eqTDD
Duration
⏱ ~1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 Lesson 19
OUTCOME
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

rust
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.

rust
// tests/api.rs
use my_crate::add;

#[test]
fn integration_add() {
    assert_eq!(add(10, 20), 30);
}

3) Common assertion macros

MacroUse
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

rust
/// 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:

rust
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:

bash
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 ones

Common 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

  1. Add 3+ unit tests in a `mod tests` block for an existing function
  2. Add one integration test under `tests/api.rs`
  3. Embed a doctest in /// comments and verify `cargo test --doc` passes
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—