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.