Testing

In the previous chapters, we've looked at modularizing a Rust application. Modules are a key component for adding unit tests to a Rust application. These modules get a special annotation, so Cargo knows that the module should be ran during testing: #[cfg(test)]

In RustRover you can easily scaffold a test module by typing tmod. A test function can then added by typing tfn. A typical unit test looks like this:

fn capitalize(value: String) -> String {
    value.to_uppercase()
}

fn main() {
    println!("{}", capitalize("Rust".to_string()));
}

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

    #[test]
    fn test_caps() {
        let input = "Rust".to_string();
        let output = capitalize(input);
        assert_eq!("RUST", output);
    }
}

There are three assertions available out-of-the-box that can be used during unit testing:

  • assert!()
  • assert_eq!()
  • assert_ne!()

You can use the play-button ▶️ in RustRover to run a single unit test, or a whole test module. Alternatively the key combination Ctrl + r does the same, based on the position of the cursor in the test module.

Cargo can also execute tests: cargo test. Remember the double-ctrl "Run Anything" operation in RustRover to easily execute Cargo commands.

Benchmark testing

Benchmarks are slightly harder to set up, as they require an external Crate to run. We use criterion.

First we'll include the criterion dependency in Cargo.toml:

[dev-dependencies]
criterion = "0.3.1"

[[bench]]
name = "capitalize"
harness = false

Then we create a benches directory that will hold our benchmark test. We'll also create a library with a single public function: capitalize_string, which is what we'll benchmark.

Our project structure looks like this:

|- Cargo.toml
|- benches
    |- capitalize.rs
|- src
    |- lib.rs

Notice that the name in the Cargo.toml file matches the capitalize.rs name in the benches directory.

Filename: src/lib.rs

pub fn capitalize_string(input: String) -> String {
    input.to_uppercase()
}

Filename: benches/capitalize.rs

#[macro_use]
extern crate criterion;
use capitalize::capitalize_string;
use criterion::Criterion;

fn bench_caps(c: &mut Criterion) {
    c.bench_function("capitalize strings", |b| {
        b.iter(|| capitalize_string("rust".to_string()))
    });
}

criterion_group!(benches, bench_caps);
criterion_main!(benches);

The || (pipe-symbols) in the benchmark code are used to create a closure. We'll look into closures in the next chapter.

You can start the test by running cargo bench.

cargo bench builds a release version of the application for benchmarking.

Criterion benchmarks the current run, compares the result to a previous run, and shows the performance delta.

Output from first run

capitalize strings      time:   [223.59 ns 225.70 ns 228.04 ns]
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high severe

Output from second run

capitalize strings      time:   [219.78 ns 222.41 ns 226.20 ns]
                        change: [-4.7158% -3.0100% -1.3074%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
  4 (4.00%) high mild
  2 (2.00%) high severe

Reference material