Rust Built-in Testing

RustUnit Testingcargo testTest FrameworkStandard Library

Rust Built-in Testing

Overview

Basic testing functionality built into the Rust standard library. Write unit tests using the #[test] attribute and run them with the cargo test command. Create tests with simple syntax without external crate dependencies, using assertion macros (assert!, assert_eq!, assert_ne!, etc.). Serves as the foundation for all Rust testing frameworks, enabling reliable tests that leverage Rust's ownership system and type safety.

Details

Key Features

  • Zero Dependencies: Works with standard library only, no external crates required
  • Simple Syntax: Define test functions with #[test] attribute
  • Parallel Execution: Runs tests in parallel by default for fast test cycles
  • Assertion Macros: Intuitive validation with assert!, assert_eq!, assert_ne!
  • Panic Testing: Verify expected panics with #[should_panic]
  • Test Organization: Separate test code with #[cfg(test)] modules
  • Documentation Tests: Automatically test code examples in comments

Architecture

Rust's testing system consists of the following components:

  • Test Runner: Test harness compiled and executed by cargo test
  • Test Attributes: Markers like #[test], #[should_panic], #[ignore]
  • Assertion Macros: Macro collection for validating test conditions
  • Test Organization: Separation of unit tests, integration tests, and documentation tests

Test Placement

  • Unit Tests: Placed in src directory modules with #[cfg(test)]
  • Integration Tests: Placed as independent files in tests/ directory
  • Documentation Tests: Automatically test code examples in documentation comments

Pros and Cons

Pros

  • Built-in: Available immediately upon Rust installation
  • Easy to Learn: Simple, intuitive syntax with comprehensive official documentation
  • Zero Overhead: No external dependencies, no increase in build time or complexity
  • Ownership Integration: Fully integrated with Rust's ownership system, ensuring safe tests
  • Parallel Execution: Runs in parallel by default for fast feedback cycles
  • Documentation Integration: Code examples function as tests, ensuring they stay up-to-date

Cons

  • Basic Features Only: Requires separate crates for parameterized tests or property-based testing
  • Assertions: Limited advanced matchers or custom messages
  • Test Fixtures: Limited mechanisms for setup/teardown
  • Parallel Control: Requires control when tests share resources

References

Code Examples

Basic Tests

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }
}

Using Assertion Macros

#[cfg(test)]
mod tests {
    #[test]
    fn test_assertions() {
        // Boolean tests
        assert!(true);
        assert!(!false);

        // Equality tests
        assert_eq!(2 + 2, 4);
        assert_ne!(2 + 2, 5);

        // Custom messages
        let result = 2 + 2;
        assert_eq!(
            result, 4,
            "Calculation result differs from expected: expected {}, got {}",
            4, result
        );
    }
}

Testing Panics

pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Division by zero is not allowed");
    }
    a / b
}

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

    #[test]
    #[should_panic]
    fn test_divide_by_zero() {
        divide(10, 0);
    }

    #[test]
    #[should_panic(expected = "Division by zero")]
    fn test_divide_by_zero_with_message() {
        divide(10, 0);
    }
}

Tests Using Result Type

#[cfg(test)]
mod tests {
    #[test]
    fn test_with_result() -> Result<(), String> {
        let result = 2 + 2;
        if result == 4 {
            Ok(())
        } else {
            Err(String::from("Calculation result is incorrect"))
        }
    }
}

Ignoring Tests

#[cfg(test)]
mod tests {
    #[test]
    fn quick_test() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    #[ignore]
    fn expensive_test() {
        // Time-consuming test
        // Not run normally
        // Can run with: cargo test -- --ignored
    }
}

Integration Tests

// tests/integration_test.rs
use my_crate;

#[test]
fn test_public_api() {
    let result = my_crate::add(2, 3);
    assert_eq!(result, 5);
}

Documentation Tests

/// Adds two numbers.
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Benchmark Tests (nightly required)

#![feature(test)]
extern crate test;

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

    #[bench]
    fn bench_add(b: &mut Bencher) {
        b.iter(|| {
            add(2, 3)
        });
    }
}

Controlling Parallel Execution

// Disable parallel execution: cargo test -- --test-threads=1
// Show output: cargo test -- --nocapture
// Run specific test: cargo test test_name

#[cfg(test)]
mod tests {
    use std::sync::Mutex;
    use std::sync::Arc;

    #[test]
    fn test_with_shared_resource() {
        // When using shared resources,
        // synchronize with Mutex etc.
        let counter = Arc::new(Mutex::new(0));
        let mut num = counter.lock().unwrap();
        *num += 1;
        assert_eq!(*num, 1);
    }
}