Phase 5Practical project

#19 Testing

#[test], assert, cargo test

Your first test

In Rust, tests are regular functions annotated with #[test]. A test passes if it does not panic. You typically place tests in a tests module at the bottom of the file:

A simple test
fn add(a: i32, b: i32) -> i32 {
    a + b
}

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

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

The #[cfg(test)] attribute tells the compiler to only compile this module when running tests. The use super::* imports everything from the parent module so you can test private functions.

Assert macros

Rust provides several macros for making assertions in tests:

assert!, assert_eq!, assert_ne!
#[cfg(test)]
mod tests {
    #[test]
    fn test_assert() {
        // assert! checks a boolean condition
        assert!(1 + 1 == 2);
        assert!(true);
    }

    #[test]
    fn test_assert_eq() {
        // assert_eq! checks that two values are equal
        assert_eq!(2 + 2, 4);
        assert_eq!("hello".to_uppercase(), "HELLO");
    }

    #[test]
    fn test_assert_ne() {
        // assert_ne! checks that two values are NOT equal
        assert_ne!(1, 2);
        assert_ne!("hello", "world");
    }

    #[test]
    fn test_with_message() {
        // Add a custom message on failure
        let result = 2 + 2;
        assert_eq!(result, 4, "Expected 4, but got {}", result);
    }
}

You can also test that code panics using #[should_panic]:

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

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

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

Testing with Result

Tests can also return Result instead of panicking. This lets you use the ? operator inside tests:

Tests that return Result
fn parse_port(s: &str) -> Result<u16, String> {
    s.parse::<u16>().map_err(|e| format!("Invalid port: {}", e))
}

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

    #[test]
    fn test_valid_port() -> Result<(), String> {
        let port = parse_port("8080")?;
        assert_eq!(port, 8080);
        Ok(())
    }

    #[test]
    fn test_invalid_port() {
        let result = parse_port("not_a_number");
        assert!(result.is_err());
    }

    #[test]
    fn test_port_range() -> Result<(), String> {
        let port = parse_port("443")?;
        assert!(port > 0, "Port must be positive");
        Ok(())
    }
}

Integration tests

Integration tests live in a tests/ directory next to src/. They test your library as an external user would:

Project structure
my-project/
  src/
    lib.rs        // Library code
    main.rs       // Binary (optional)
  tests/
    integration_test.rs   // Integration test
src/lib.rs
pub fn greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}

pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}
tests/integration_test.rs
// No #[cfg(test)] needed -- the tests/ directory is only compiled for testing
use testing_demo::{greeting, is_even};

#[test]
fn test_greeting() {
    assert_eq!(greeting("Rust"), "Hello, Rust!");
}

#[test]
fn test_greeting_empty() {
    assert_eq!(greeting(""), "Hello, !");
}

#[test]
fn test_even_numbers() {
    assert!(is_even(2));
    assert!(is_even(0));
    assert!(is_even(-4));
}

#[test]
fn test_odd_numbers() {
    assert!(!is_even(1));
    assert!(!is_even(3));
    assert!(!is_even(-7));
}

Running tests

Cargo provides several ways to run tests:

Running tests
# Run all tests
cargo test

# Run tests with output (println! visible)
cargo test -- --nocapture

# Run a specific test by name
cargo test test_add

# Run tests matching a pattern
cargo test greeting

# Run only integration tests
cargo test --test integration_test

# Run tests in a specific module
cargo test tests::test_add

# Run ignored tests
cargo test -- --ignored

You can mark tests as ignored with #[ignore]. This is useful for slow tests or tests that require external resources:

Ignoring tests
#[test]
#[ignore]
fn expensive_test() {
    // This test takes a long time
    // Only runs with: cargo test -- --ignored
    std::thread::sleep(std::time::Duration::from_secs(10));
    assert!(true);
}

Summary

Quick reference
#[cfg(test)]                  // Only compile for tests
mod tests {
    use super::*;             // Import parent module

    #[test]                   // Mark as test
    fn test_name() { }

    #[test]
    #[should_panic]           // Expect a panic
    fn test_panic() { }

    #[test]
    #[ignore]                 // Skip by default
    fn slow_test() { }
}

assert!(condition);           // Check boolean
assert_eq!(left, right);     // Check equality
assert_ne!(left, right);     // Check inequality

cargo test                    // Run all tests
cargo test -- --nocapture     // Show println output
cargo test name               // Filter by name

Your turn

Try running the tests with cargo test, see the output with cargo test -- --nocapture, and run the program with cargo run:

terminal — cargo
user@stemlegacy:~/testing-demo$