Chapter 11 - Writing Automated Tests

At its simplest, a test in Rust is a function that’s annotated with the test attribute. Attributes are metadata about pieces of Rust code:


#![allow(unused)]
fn main() {
#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}
}

Various helper macros useful for testing:

  • assert!
  • assert_eq!
  • assert_ne!

You can also add a custom message to be printed with the failure message as optional arguments to the assert!, assert_eq!, and assert_ne! macros. Any arguments specified after the one required argument to assert! or the two required arguments to assert_eq! and assert_ne! are passed along to the format! macro:


#![allow(unused)]
fn main() {
#[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

We place the #[should_panic] attribute after the #[test] attribute and before the test function it applies to.


#![allow(unused)]
fn main() {
#[test]
#[should_panic]
fn greater_than_100() {
    panic("hello");
}
}

To make should_panic tests more precise, we can add an optional expected parameter to the should_panic attribute. The test harness will make sure that the failure message contains the provided text.

Using Result<T, E> in Tests


#![allow(unused)]
fn main() {
#[test]
fn it_works() -> Result<(), String> {
    if 2 + 2 == 4 {
        Ok(())
    } else {
        Err(String::from("two plus two does not equal four"))
    }
}
}

Controlling How Tests Are Run

The default behavior of the binary produced by cargo test is to run all the tests in parallel and capture output generated during test runs, preventing the output from being displayed and making it easier to read the output related to the test results.

Various test options

  • When you run multiple tests, by default they run in parallel using threads.
$ cargo test -- --test-threads=1
$ cargo test -- --nocapture
$ cargo test -- --ignored # Runs only the ignored tests

Test Organization

  • Unit tests are small and more focused, testing one module in isolation at a time, and can test private interfaces.
  • Integration tests are entirely external to your library and use your code in the same way any other external code would, using only the public interface and potentially exercising multiple modules per test.

Unit tests

The convention is to create a module named tests in each file to contain the test functions and to annotate the module with cfg(test).

The #[cfg(test)] annotation on the tests module tells Rust to compile and run the test code only when you run cargo test, not when you run cargo build. Note that cfg stands for configuration.

Integration Tests

We create a tests directory at the top level of our project directory, next to src. Cargo knows to look for integration test files in this directory.

Note that we can create tests/common/mod.rs to put helper functions. Rust understands this naming convention and treats the common module not as an integration tests file.