Skip to content

Rust Testing & Documentation — Unit Tests, Integration & Doc Tests

DodaTech Updated 2026-06-21 6 min read

In this tutorial, you'll learn about Rust Testing & Documentation. We cover key concepts, practical examples, and best practices.

Rust integrates testing and documentation directly into the language, with unit tests, integration tests, doc tests, and rustdoc all working together to produce tested, documented code from a single source.

What You'll Learn

In this tutorial, you'll learn how Rust testing works: writing unit tests with #[cfg(test)], creating integration tests in the tests/ directory, writing documentation that is tested as code, running benchmarks, and generating HTML documentation with cargo doc.

Why It Matters

Systems code must be reliable. Rust's integrated testing framework makes it easy to write and run tests without external tools. Doc tests ensure your code examples never go out of date. Generated documentation with rustdoc is the standard for the Rust ecosystem.

Real-World Use

The Rust standard library has over 100,000 tests. The serde library uses doc tests for all examples. The tokio project uses integration tests for runtime behavior. Durga Antivirus Pro integrates tests directly with CI to verify scan engine correctness on every commit.

flowchart TD
    SRC[Source Code] --> UNIT[Unit Tests: #[test]]
    SRC --> DOC[Doc Tests: /// ```]
    SRC --> INT[Integration Tests: tests/]
    SRC --> RUSTDOC[rustdoc: HTML docs]
    UNIT --> CGO[cargo test]
    DOC --> CGO
    INT --> CGO
    CGO --> REPORT[Test Report]
    RUSTDOC --> DOCS[Generated Documentation]
â„šī¸ Info

Prerequisites: Structs & Enums, Error Handling, and basic Rust knowledge.

Unit Tests

Unit tests are written alongside code using #[cfg(test)].

pub fn calculate_checksum(data: &[u8]) -> u32 {
    data.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32))
}

pub fn validate_checksum(data: &[u8], expected: u32) -> bool {
    calculate_checksum(data) == expected
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_checksum_empty() {
        assert_eq!(calculate_checksum(&[]), 0);
    }

    #[test]
    fn test_checksum_single_byte() {
        assert_eq!(calculate_checksum(&[42]), 42);
    }

    #[test]
    fn test_checksum_multiple_bytes() {
        assert_eq!(calculate_checksum(&[1, 2, 3]), 6);
    }

    #[test]
    fn test_validate_checksum() {
        let data = vec![10, 20, 30];
        let checksum = calculate_checksum(&data);
        assert!(validate_checksum(&data, checksum));
    }
}

Integration Tests

Integration tests live in the tests/ directory and test your crate as an external consumer.

// In tests/integration_test.rs:
use my_crate::calculate_checksum;

#[test]
fn integration_test_large_data() {
    let data = vec![255u8; 1024];
    let checksum = calculate_checksum(&data);
    assert_eq!(checksum, 255u32.wrapping_mul(1024));
}

#[test]
fn integration_test_known_values() {
    let data = b"Hello, World!";
    let checksum = calculate_checksum(data);
    assert!(checksum > 0);
    assert!(checksum < 2000);
}

Doc Tests

Documentation examples are automatically tested. This keeps your docs up to date.

/// Calculates a simple additive checksum over a byte slice.
///
/// This function sums all bytes with wrapping addition, suitable for
/// simple integrity verification in network protocols.
///
/// # Examples
///
/// ```
/// use my_crate::calculate_checksum;
///
/// let data = vec![1, 2, 3, 4];
/// let sum = calculate_checksum(&data);
/// assert_eq!(sum, 10);
/// ```
///
/// # Errors
///
/// This function does not fail for any input, including empty slices.
pub fn calculate_checksum(data: &[u8]) -> u32 {
    data.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32))
}

/// Validates that a checksum matches the expected value.
///
/// ```
/// use my_crate::{calculate_checksum, validate_checksum};
///
/// let data = b"test data";
/// let cs = calculate_checksum(data);
/// assert!(validate_checksum(data, cs));
/// assert!(!validate_checksum(data, cs + 1));
/// ```
pub fn validate_checksum(data: &[u8], expected: u32) -> bool {
    calculate_checksum(data) == expected
}

Test Organization

Tests can use different attributes for setup, expected failures, and ignoring.

pub fn parse_config_line(line: &str) -> Result<(String, String), String> {
    let parts: Vec<&str> = line.splitn(2, '=').collect();
    if parts.len() != 2 {
        return Err("Missing '=' separator".to_string());
    }
    Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_valid_config_line() {
        let (key, val) = parse_config_line("name=Rust").unwrap();
        assert_eq!(key, "name");
        assert_eq!(val, "Rust");
    }

    #[test]
    fn test_config_with_whitespace() {
        let (key, val) = parse_config_line("  key = value  ").unwrap();
        assert_eq!(key, "key");
        assert_eq!(val, "value");
    }

    #[test]
    #[should_panic(expected = "Missing")]
    fn test_invalid_config() {
        parse_config_line("invalid_line").unwrap();
    }

    #[test]
    fn test_multiple_equals() {
        let (key, val) = parse_config_line("key=val1=val2").unwrap();
        assert_eq!(key, "key");
        assert_eq!(val, "val1=val2");
    }

    #[test]
    #[ignore = "Requires external config file"]
    fn test_external_config() {
        // Integration test that requires a file
    }
}

Test Attributes and Patterns

/// Tests can be grouped into modules and use setup helpers.
#[cfg(test)]
mod tests {
    #[test]
    fn assert_macros() {
        assert!(true);
        assert_eq!(1, 1);
        assert_ne!(1, 2);
        // Custom message
        assert!(1 + 1 == 2, "Basic math works");
    }

    #[test]
    fn result_returning_test() -> Result<(), String> {
        let val = "42".parse::<i32>().map_err(|_| "Parse failed")?;
        assert_eq!(val, 42);
        Ok(())
    }
}

Common Mistakes

1. Not Using #[cfg(test)] for Test Modules

Test helper functions used only in tests should be behind #[cfg(test)] to avoid compiling them in release builds.

2. Integration Tests in src/

Integration tests must go in the tests/ directory at the crate root. Tests in src/ are unit tests that can access private items.

3. Doc Tests That Do Not Compile

Doc test examples must be valid Rust code. Run cargo test --doc to verify. Use ignore or no_run attributes for non-compiling or non-runnable examples.

4. Not Testing Error Cases

Test both success and failure paths. Use #[should_panic] or Result<T, E> returning tests for error cases.

5. Flaky Tests

Tests that depend on external state (network, filesystem, time) can fail intermittently. Mock external dependencies or use deterministic alternatives.

Practice Questions

1. What is the difference between unit tests and integration tests in Rust? Unit tests live in src/ and can test private functions. Integration tests live in tests/ and test the public API from an external perspective.

2. How do doc tests work? Code examples in /// documentation comments are compiled and run during cargo test --doc. If the example does not compile or panics, the test fails.

3. What is the #[should_panic] attribute? It marks a test that is expected to panic. If the test panics, it passes. If it completes without panicking, it fails.

4. How do you run only specific tests? Use cargo test test_name to run tests matching a pattern, or #[ignore] to exclude tests, then cargo test -- --ignored to run only ignored tests.

5. Challenge: Create a test suite for a Config parser with unit tests for parsing, integration tests for file reading, and doc tests for examples. Include edge cases for empty files and malformed input.

Mini Project: Test Suite for String Utility

/// Truncates a string to the given length, appending "..." if truncated.
///
/// # Examples
///
/// ```
/// use my_crate::truncate;
///
/// assert_eq!(truncate("hello world", 5), "hello...");
/// assert_eq!(truncate("hello", 10), "hello");
/// assert_eq!(truncate("", 3), "");
/// ```
pub fn truncate(s: &str, max_len: usize) -> String {
    if s.len() <= max_len {
        s.to_string()
    } else {
        format!("{}...", &s[..max_len])
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_truncate_short() {
        assert_eq!(truncate("abc", 10), "abc");
    }

    #[test]
    fn test_truncate_exact() {
        assert_eq!(truncate("abcde", 5), "abcde");
    }

    #[test]
    fn test_truncate_long() {
        assert_eq!(truncate("hello world", 5), "hello...");
    }

    #[test]
    fn test_truncate_empty() {
        assert_eq!(truncate("", 5), "");
    }

    #[test]
    fn test_truncate_unicode() {
        assert_eq!(truncate("hello", 3), "hel...");
    }

    #[test]
    fn test_truncate_zero_max() {
        assert_eq!(truncate("hello", 0), "...");
    }
}

FAQ

What is the command to run tests in Rust?

cargo test runs all tests (unit, integration, doc). cargo test --lib runs only unit tests. cargo test --doc runs only doc tests. cargo test test_name runs tests matching a name pattern.

Can doc tests be hidden from documentation?

Yes. Use /// ```ignore for code that is hidden from docs but still tested, or /// ```no_run for code that compiles but does not execute.

How do I generate HTML documentation?

cargo doc --open generates documentation for your crate and all dependencies, then opens it in your browser. cargo doc --no-deps generates docs only for your crate.

Error Handling
Cargo Workspaces
Performance Optimization

What's Next

Learn Cargo Workspaces for managing multi-crate projects, and Design Patterns for writing idiomatic testable code.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro