Code of the Day
AdvancedEcosystem & tooling

Testing

Write unit tests, integration tests, and doc tests in Rust — and run them efficiently with cargo test.

RustAdvanced11 min read
Recommended first
By the end of this lesson you will be able to:
  • 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

MacroPasses 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

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 --doc

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 file

Tests 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. 1.
    Why is the test module marked with #[cfg(test)]?
  2. 2.
    What can integration tests in tests/ access?
  3. 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:

  1. A unit test that verifies the happy path.
  2. A test with #[should_panic] for an invalid input.
  3. An integration test in tests/ that exercises the public API end-to-end.
  4. 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!.

Finished reading? Mark it complete to track your progress.

On this page