Skip to main content

Writing Tests in Rust

Writing Tests in Rust

Testing is an essential part of software development, ensuring that code behaves as expected and preventing regressions. Rust provides a built-in testing framework that allows developers to write unit tests, integration tests, and documentation tests efficiently.


1. Understanding Rust’s Testing Framework

Rust's testing framework is integrated into cargo, making it easy to run tests with a single command. Rust supports:

  • Unit Tests: Test individual functions or modules.
  • Integration Tests: Test the public interface of a crate.
  • Documentation Tests: Ensure code examples in documentation work correctly.

To run tests, use:

cargo test

2. Writing Unit Tests

Unit tests focus on testing individual components in isolation. They are placed in the same file as the implementation but inside a #[cfg(test)] module.

<!-- Example of a Unit Test -->
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);
    }
}

Explanation:

  • The #[test] attribute marks the function as a test.
  • assert_eq! checks if the function returns the expected result.
  • The #[cfg(test)] attribute ensures the test module is only compiled during testing.

3. Running Tests and Viewing Output

By default, cargo test runs all tests in parallel. To see detailed output, use:

cargo test -- --nocapture

To run a specific test:

cargo test test_add

4. Handling Expected Failures

Sometimes, we want to test if a function correctly handles errors. Rust provides #[should_panic] to mark tests that are expected to fail.

<!-- Expected Panic Test -->
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Cannot divide by zero!");
    }
    a / b
}

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

Explanation:

  • The #[should_panic] attribute ensures the test passes if it panics.
  • The expected argument checks the panic message.

5. Using Result in Tests

Tests can return Result<(), E> instead of panicking:

<!-- Using Result in Tests -->
fn is_even(n: i32) -> bool {
    n % 2 == 0
}

#[test]
fn test_is_even() -> Result<(), String> {
    if is_even(4) {
        Ok(())
    } else {
        Err("4 should be even".to_string())
    }
}

This approach is useful when working with error-handling mechanisms.


6. Writing Integration Tests

Integration tests test the public API of a crate and are placed inside the tests directory.

mkdir tests

Example integration test (tests/integration_test.rs):

<!-- Integration Test -->
use my_crate::add;

#[test]
fn test_addition() {
    assert_eq!(add(2, 2), 4);
}

Integration tests do not use mod tests because they test the crate as a whole.


7. Documentation Tests

Rust can extract and test code snippets from documentation comments:

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

Run documentation tests with:

cargo test --doc

8. Running Tests Selectively

Rust allows running tests selectively using filters:

  • Run tests matching a name: cargo test test_name
  • Ignore specific tests with #[ignore].
<!-- Ignored Test -->
#[test]
#[ignore]
fn slow_test() {
    // This test will be skipped unless explicitly run
}

To run ignored tests:

cargo test -- --ignored

9. Conclusion

  • Rust provides built-in support for unit, integration, and documentation tests.
  • Use #[test] for unit tests and #[should_panic] for expected failures.
  • Integration tests reside in the tests directory.
  • Documentation tests ensure code examples in documentation remain valid.
  • Rust’s testing framework makes it easy to verify correctness with cargo test.

By leveraging Rust’s robust testing framework, developers can write safer and more reliable code.

Comments