24 January 2023
Like other modern programming languages, Rust has support for automated tests built-in. In fact,
when you set up a library with cargo new --lib
, it’ll generate a sample function with a
sample unit test.
Unit tests are usually included in the same source file as the domain code, in a nested module
called test
. Here’s an example testing a factorial function:
fn factorial(n: usize) -> usize {
if n <= 1 { 1 } else { n * factorial(n - 1) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn factorial_works() {
assert!(factorial(1) == 1);
}
}
The tests
module is annotated with #[cfg(test)]
so that it will only be
compiled when running tests. If we run the tests with cargo test
, cargo builds an
executable that runs each function annotated with #[test]
as a test. Other functions
are not run, so you can have helper functions. The line
use super::*;
imports all definitions in the outer module, including private ones.
If a test function panics, the test counts as failed; otherwise it has succeeded. Most tests use one of the three assertion macros:
#[test]
fn factorial_works() {
assert!(factorial(1) == 1); // assert true
assert_eq!(factorial(1), 1); // assert equal
assert_ne!(factorial(1), 0); // assert not equal
}
You can set a custom message for assertions with optional parameters that are passed to the
format!
macro. We can use that for a table-driven test following the Go convention:
#[test]
fn table_driven() {
let cases = vec![(0, 1), (1, 1), (2, 2), (3, 6), (4, 24), (5, 120)];
for (n, want) in cases {
let got = factorial(n);
assert_eq!(got, want,
"factorial({}) == {}, want {}", n, got, want);
}
}
If you have code that’s supposed to panic, use the should_panic
attribute to test it:
#[test]
#[should_panic]
fn test_vector_panic() {
vec![][0]
}
The test will count as failed if it doesn’t panic.
Test functions can also return Result
, with Ok
indicating a successful
test run and Err
or a panic a failed one. This lets you use the question mark operator
in tests:
#[test]
fn read_to_string_works() -> std::io::Result<()> {
let text = fs::read_to_string("sample-file.txt")?;
assert_eq!(text, "Sample text\n");
Ok(())
}
If you have a test that shouldn’t always run, for example because it takes a long time, annotate it
with #[ignore]
:
#[test]
#[ignore]
fn slow_test() {
assert_eq!(process(big_dataset()), 42);
}
This test will be skipped by default; to run it, use
cargo test -- --ignored
We need the --
because we’re passing an argument to the test binary, not to cargo. To
run all tests:
cargo test -- --include-ignored
cargo test
will also run code in documentation comments; if the code panics, the test
fails. This helps to catch errors in the documentation early. Here’s the factorial function again:
/// Compute the factorial of n.
///
/// ```
/// let result = mycrate::factorial(5);
/// assert_eq!(result, 120);
/// ```
pub fn factorial(n: usize) -> usize {
if n <= 1 { 1 } else { n * factorial(n - 1) }
}
There’s a third category of tests run by cargo test
after unit and doc tests:
integration tests. To set up integration tests, create a directory tests
at the same
level as src
. Each file there will be compiled as a separate crate; functions marked
with #[test]
will be considered tests just as for unit tests. Here’s an integration
test for the factorial function:
use mycrate;
#[test]
fn factorial_of_six() {
assert_eq!(mycrate::factorial(6), 720);
}
Integration tests can only use the public interface of the module. Their purpose is usually to test that various parts of your library work together -- hence the name integration test.