11. Writing Automated Tests

Learning objectives

  • Understand how to write tests in Rust
  • Learn the anatomy of a test function
  • Use macros like assert!, assert_eq!, and assert_ne! in tests
  • Know how to check for panics with should_panic
  • Learn how to control test execution
  • Understand test organization: unit tests and integration tests

Introduction

Testing in Rust

  • Tests ensure code functions as expected

  • Rust (cargo) provides native support for automating tests

  • Types of test in Rust include:

    • Unit tests
    • Integration tests
    • Documentation tests
    • Benchmarks

How to Write Tests

Typical Flow of a Test

  • Set up any needed data or state.
  • Run the code you want to test.
  • Assert that the results are what you expect.

Anatomy of a Test Function

  • Annotate functions with #[test] to make them tests

    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
  • Run tests with cargo test

The assert! Macro

  • Ensures a condition evaluates to true

    assert!(condition);
  • Example:

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle { width: 8, height: 7 };
        let smaller = Rectangle { width: 5, height: 1 };
    
        assert!(larger.can_hold(&smaller));
    }

The assert_eq! and assert_ne! Macros

  • assert_eq!(left, right) checks left == right

  • assert_ne!(left, right) checks left != right

  • Provide detailed error messages on failure

  • Example:

    #[test]
    fn it_adds_two() {
        assert_eq!(add_two(2), 4);
    }

Adding Custom Failure Messages

  • Add custom messages to assertions

    assert!(condition, "Custom message");
  • Example:

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{}`",
            result
        );
    }

Checking for Panics with should_panic

  • Use #[should_panic] to test code that should panic

    #[test]
    #[should_panic]
    fn test_panics() {
        // code that should panic
    }
  • Use expected to specify a substring of the panic message

    #[test]
    #[should_panic(expected = "must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }

Using Result<T, E> in Tests

  • Tests can return Result<T, E> instead of panicking

    #[test]
    fn it_works() -> Result<(), String> {
        // code that might return Err
        Ok(())
    }
  • Allows use of the ? operator in tests

Controlling How Tests Are Run

Running Tests in Parallel or Consecutively

  • Tests run in parallel by default

  • To run tests consecutively:

    cargo test -- --test-threads=1

Showing Function Output

  • Output from println! is captured by default

  • To display output even for passing tests:

    cargo test -- --show-output

Running a Subset of Tests by Name

Running Single Tests

  • Run a specific test by specifying its name:

    cargo test test_name

Filtering Multiple Tests

  • Run tests matching a pattern:

    cargo test pattern

Ignoring Some Tests Unless Specifically Requested

  • Use #[ignore] to exclude tests by default

    #[test]
    #[ignore]
    fn expensive_test() {
        // code that takes a long time
    }
  • Run ignored tests with:

    cargo test -- --ignored

Test Organization

Unit Tests

  • Test individual units of code in isolation
  • Placed in the same file as the code under test

The Tests Module and #[cfg(test)]

  • Place tests in a tests module annotated with #[cfg(test)]

  • This module is only compiled when testing

    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn it_works() {
            // test code
        }
    }

Testing Private Functions

  • You can test private functions in Rust

  • Example:

    fn internal_adder(a: i32, b: i32) -> i32 {
        a + b
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_internal() {
            assert_eq!(internal_adder(2, 2), 4);
        }
    }

Integration Tests

  • Test the public API as an external user would

  • Placed in the tests directory

  • Each file in tests is a separate crate

    my_project
    ├── Cargo.toml
    ├── src
    │   └── lib.rs
    └── tests
        └── integration_test.rs
  • Example:

    use my_project;
    
    #[test]
    fn it_adds_two() {
        assert_eq!(my_project::add_two(2), 4);
    }

Submodules in Integration Tests

  • Share code between integration tests using modules

  • Create tests/common/mod.rs for shared code

    // tests/common/mod.rs
    pub fn setup() {
        // setup code
    }
    
    // tests/integration_test.rs
    mod common;
    
    #[test]
    fn it_adds_two() {
        common::setup();
        assert_eq!(my_project::add_two(2), 4);
    }

Integration Tests for Binary Crates

  • Binary crates (with only main.rs) can’t be tested directly via integration tests
  • Solution: Extract logic into a library crate (lib.rs)
  • main.rs can then call into lib.rs

Summary

  • Rust provides powerful tools for writing automated tests
  • Use unit tests to test small pieces of code
  • Use integration tests to test how pieces work together
  • Control test execution with command-line options
  • Organize tests effectively to maintain a robust codebase