TechLead
Lesson 16 of 28
5 min read
Rust

Testing in Rust

Learn Rust testing with #[test], assert macros, unit and integration tests, test filtering, mocking strategies, and property-based testing.

Testing in Rust

Rust has first-class testing support built into the language and toolchain. Tests live alongside your code, run with cargo test, and the compiler ensures your test infrastructure compiles correctly alongside your main code.

Unit Tests

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn divide(a: f64, b: f64) -> Result {
    if b == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

// Tests module — only compiled when running tests
#[cfg(test)]
mod tests {
    use super::*; // Import everything from parent module

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

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

    #[test]
    fn test_divide() {
        let result = divide(10.0, 2.0).unwrap();
        assert!((result - 5.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_divide_by_zero() {
        let result = divide(10.0, 0.0);
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "Division by zero");
    }

    #[test]
    #[should_panic(expected = "out of bounds")]
    fn test_panic() {
        let v = vec![1, 2, 3];
        let _ = v[99]; // Panics with index out of bounds
    }

    #[test]
    fn test_with_result() -> Result<(), String> {
        let result = divide(10.0, 2.0)?;
        assert!((result - 5.0).abs() < f64::EPSILON);
        Ok(())
    }

    #[test]
    #[ignore] // Skip unless explicitly requested
    fn expensive_test() {
        // Run with: cargo test -- --ignored
        std::thread::sleep(std::time::Duration::from_secs(10));
    }
}

Assert Macros

#[cfg(test)]
mod tests {
    #[test]
    fn demonstrate_asserts() {
        // Basic assertions
        assert!(true);
        assert_eq!(1 + 1, 2);       // Left == Right
        assert_ne!(1, 2);            // Left != Right

        // Custom error messages
        let age = 17;
        assert!(
            age >= 18,
            "Expected age >= 18, but got {age}"
        );

        // Floating-point comparison
        let result = 0.1 + 0.2;
        assert!(
            (result - 0.3).abs() < f64::EPSILON,
            "Float comparison: {result} != 0.3"
        );

        // debug_assert! — only in debug builds
        debug_assert!(expensive_check(), "Debug-only assertion");
    }

    fn expensive_check() -> bool { true }
}

Integration Tests

// Integration tests live in tests/ directory
// tests/
//   integration_test.rs
//   common/
//     mod.rs

// --- tests/integration_test.rs ---
use my_crate::add; // Test the public API

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

// --- tests/common/mod.rs ---
// Shared test helpers (not run as a test file)
pub fn setup_test_db() -> TestDb {
    // Setup code
    TestDb::new()
}

// --- tests/db_test.rs ---
mod common;

#[test]
fn test_with_db() {
    let db = common::setup_test_db();
    // Test with database
}

// Run specific tests:
// cargo test                       # All tests
// cargo test test_add              # Tests matching "test_add"
// cargo test --test integration    # Only integration tests
// cargo test --lib                 # Only unit tests
// cargo test -- --nocapture        # Show stdout output
// cargo test -- --ignored          # Run #[ignore] tests

Mocking and Property-Based Testing

// Mocking with mockall
// Cargo.toml: mockall = "0.13"

use mockall::automock;

#[automock]
trait Database {
    fn get_user(&self, id: u64) -> Option;
    fn save_user(&self, name: &str) -> Result;
}

fn greet_user(db: &impl Database, id: u64) -> String {
    match db.get_user(id) {
        Some(name) => format!("Hello, {name}!"),
        None => "User not found".to_string(),
    }
}

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

    #[test]
    fn test_greet_existing_user() {
        let mut mock = MockDatabase::new();
        mock.expect_get_user()
            .with(eq(1))
            .returning(|_| Some("Alice".to_string()));

        assert_eq!(greet_user(&mock, 1), "Hello, Alice!");
    }

    #[test]
    fn test_greet_missing_user() {
        let mut mock = MockDatabase::new();
        mock.expect_get_user()
            .returning(|_| None);

        assert_eq!(greet_user(&mock, 999), "User not found");
    }
}

// Property-based testing with proptest
// Cargo.toml: proptest = "1"
// use proptest::prelude::*;
//
// proptest! {
//     #[test]
//     fn test_add_commutative(a in -1000..1000i32, b in -1000..1000i32) {
//         assert_eq!(add(a, b), add(b, a));
//     }
//
//     #[test]
//     fn test_add_identity(a in -1000..1000i32) {
//         assert_eq!(add(a, 0), a);
//     }
// }

Key Takeaways

  • ✅ Unit tests live in #[cfg(test)] mod tests within the same file
  • ✅ Integration tests live in the tests/ directory and test the public API
  • ✅ Use assert!, assert_eq!, assert_ne! with custom messages
  • #[should_panic] tests expected panics; #[ignore] skips slow tests
  • ✅ Use mockall for mocking traits and proptest for property-based testing

Continue Learning