kota's memex

Rust includes testing support natively. Tests are just functions that verify that your non-test code is functioning how you expect.

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

#[test] and #[cfg(test)]

In rust a test is a function annotated with the #[test] attribute.

integration

By convention, unit tests are in the same file as the code they are testing while integration tests (or blackbox tests) are usually in a separate tests directory:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

http integration tests

Using tokio and reqwest you can spin up your rust webserver and just make http calls to it:

use std::net::TcpListener;

fn spawn_app() -> String {
        let listener = TcpListener::bind("127.0.0.1:0")
                .expect("Failed to bind to random port");
        let port = listener.local_addr().unwrap().port();
        let server = zero2prod::run(listener).expect("Failed to bind to address");
        let _ = tokio::spawn(server);
        format!("http://127.0.0.1:{}", port)
}

#[tokio::test]
async fn health_check_works() {
        let address = spawn_app();
        let client = reqwest::Client::new();

        let response = client
                .get(&format!("{}/health_check", &address))
                .send()
                .await
                .expect("Failed to execute request.");

        assert!(response.status().is_success());
        assert_eq!(Some(0), response.content_length());
}

unit

Since unit tests are in the same file it's again convention to create a submodule in that file called tests this module is then annotated with #[cfg(test)] which tells cargo only to build this module if cargo test is called. Items in child modules can use the items in their ancestor modules and since we bring them all into scope with super::* we can test private functions in our unit tests.

super::*

Note the use super::*; line inside the tests module. The tests module is a regular module that follows the usual visibility rules. Because tests is an inner module we need to bring the code under test in the outer module into scope of the inner module. We use a glob pattern here so anything in the outer module is brought into the scope of the inner tests module.

assert, assert_eq, and assert_ne

Rust provides two macros assert to verify that an expression is true and assert_eq to verify that two values are equal. This is very handy in tests. The advantage of assert_eq(a, b) over assert(a == b) is that the former will tell you why it failed rather than just that it got false.

The assert macros can all take additional arguments to format a custom error message:

assert_eq!(a, b, "we are testing addition with {} and {}", a, b);

should panic

If a test is meant to induce a panic you can use the should panic attribute:

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

    #[test]
    #[should_panic]
    fn negative_width() {
        let _rect = Rectangle::new(-10, 10);
    }

    #[test]
    #[should_panic]
    fn negative_height() {
        let _rect = Rectangle::new(10, -10);
    }
}

using Result in tests

We can specify a return type of Result which allows using the ? operator:

#[test]
fn it_works() -> Result<(), String> {
    let result = add(2, 2);

    if result == 4 {
        Ok(())
    } else {
        Err(String::from("two plus two does not equal four"))
    }
}

ignore by default

Some tests may take a very long time or connect to the internet or something. To ignore them by default:

#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}

We can run only the ignored tests with:

cargo test -- --ignored

Or all tests with:

cargo test -- --include-ignored

testing binary crates

If our project is a binary crate that only contains a src/main.rs file and doesn’t have a src/lib.rs file, we can’t create integration tests in the tests directory and bring functions defined in the src/main.rs file into scope with a use statement. Only library crates expose functions that other crates can use; binary crates are meant to be run on their own.

This is one of the reasons Rust projects that provide a binary have a straightforward src/main.rs file that calls logic that lives in the src/lib.rs file. Using that structure, integration tests can test the library crate with use to make the important functionality available. If the important functionality works, the small amount of code in the src/main.rs file will work as well, and that small amount of code doesn’t need to be tested.