Rust Testing

Unit Tests

Tests live alongside the code
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);
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow() {
        add(i32::MAX, 1);  // panics
    }
}

[cfg(test)] compiles the module only during testing. [test] marks a function as a test. assert_eq!, assert_ne!, and assert! are the assertion macros.

Running Tests

cargo test commands
cargo test                        # all tests
cargo test test_add               # by name substring
cargo test -- --nocapture         # show println! output
cargo test -- --test-threads=1    # run sequentially
cargo test --lib                  # only unit tests
cargo test --doc                  # only doc tests
cargo test --test integration     # specific integration test file

Tests run in parallel by default. Use --test-threads=1 when tests share resources.

Result-Based Tests

Return Result instead of panicking
#[test]
fn test_parse_config() -> Result<(), Box<dyn std::error::Error>> {
    let port: u16 = "8080".parse()?;
    assert_eq!(port, 8080);
    Ok(())
}

Returning Result lets you use ? in tests. The test fails if Err is returned.

Integration Tests

tests/ directory — test the public API
// tests/integration_test.rs
use netcheck::scanner;

#[test]
fn test_scan_localhost() {
    let result = scanner::scan("127.0.0.1", &[80, 443]);
    assert!(!result.is_empty());
}

Integration tests live in tests/ at the crate root. They can only access public APIs. Each file compiles as a separate crate.

Test Organization

Shared test helpers
// tests/common/mod.rs — shared setup code
pub fn setup_test_env() -> TestEnv {
    TestEnv::new()
}

// tests/scanner_test.rs
mod common;

#[test]
fn test_with_setup() {
    let env = common::setup_test_env();
    // test code...
}

Custom Assertions

Assertion with context
#[test]
fn test_validate_port() {
    let port = 8080;
    assert!(
        port > 0 && port <= 65535,
        "port {port} out of valid range [1, 65535]"
    );

    assert_eq!(
        validate_port(0),
        Err(ValidationError::OutOfRange),
        "port 0 should be rejected"
    );
}

The third argument to assert!, assert_eq!, and assert_ne! is a format string displayed on failure.

Doc Tests

Code in documentation is tested
/// Parses a port number from a string.
///
/// # Examples
///
/// ```
/// use netcheck::parse_port;
///
/// assert_eq!(parse_port("8080"), Ok(8080));
/// assert!(parse_port("abc").is_err());
/// ```
///
/// # Errors
///
/// Returns `Err` if the string is not a valid port number.
pub fn parse_port(s: &str) -> Result<u16, ParseError> {
    let port: u16 = s.parse()?;
    if port == 0 { return Err(ParseError::Zero); }
    Ok(port)
}

Code blocks in doc comments are compiled and run by cargo test --doc. This keeps documentation examples from going stale.