Journey with Rust Part 2: Unit testing

I won my first battles with Rust compiler, and now I have a code that is failproof. But having software that does not crash is not enough. I need to make sure, that my application does what it is supposed to do. Even more important, I need a guarantee that its behavior won’t change in the future, after all the refactoring, bug fixing, and adding new features. Also I need a magic force to drive my architecture (so I can add yet another cool abbreviation in my CV). What I need is Unit Testing.

Time for another adventure. Let the google search engine guide us on our journey.

Simple test

Our first goal is to test this piece of code (full source can be found here).

pub fn find_marmot(depth: u32) -> bool {
    depth > 20
}

A very nice surprise is that Rust already comes with support for unit testing so there is no need to install any external crate. Testing is as simple as adding few extra lines of code into the source file…

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

    #[test]
    fn when_search_not_deep_then_no_marmot_found() {
        assert_eq!(find_marmot(19), false);
    }
}

…and running the test target.

$cargo test
   Compiling marmot_test v0.1.0
    Finished test
     Running unittests

running 1 test
test marmot_hole::test::when_search_not_deep_then_no_marmot_found ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Super easy but also super primitive*. It is not something that you could compare with a proper unit test framework like Google Test or Catch2. It looks more like a small addon that allows you to run multiple small programs and count the number of panics. No extra features like test fixtures, mocks or parameterized tests.

Less simple test

This is our following function to test:

pub fn chance_to_find_marmot(day_of_weeek: u8) -> f32 {
    match day_of_weeek {
        1 => 1.0,
        2 | 3 | 4 => 1.0 / 2.0,
        5 | 6 | 7 => 1.0 / 3.0,
        _ => panic!("Not a day of week")
    }
}

While I can test for panic with a should_panic attribute, there is no check dedicated for floating point results that would not suffer from floating point inaccuracy (see EXPECT_FLOAT_EQ from GTest). To add this functionality, we need to install assert_approx_eq crate and use it like this:

#[test]
#[should_panic]
fn when_invalid_week_day_should_panic() {
    chance_to_find_marmot(9);
}

#[test]
fn when_friday_than_0_33_chance_of_fiding_marmot() {
    let res = chance_to_find_marmot(5);
    assert_approx_eq!(res, 0.333, 0.01)
}

Parameterized tests

Searching for a way to do parameterized tests I encounter rstest crate. So it looks that I will have to install yet another external framework. With rstest installed I can write my test like this:

#[rstest]
#[case(1, 1.0)]
#[case(2, 0.5)]
#[case(3, 0.5)]
#[case(4, 0.5)]
#[case(5, 0.33)]
#[case(6, 0.33)]
#[case(7, 0.33)]
fn check_all_days(#[case] input: u8,#[case] expected: f32) {
assert_approx_eq!(expected, chance_to_find_marmot(input), 0.01)
}

It also supports test fixtures but does not offer anything more than that.

Mocking framework

Now we start moving even more uphill. There are so many different mocking frameworks for Rust that it is really hard to judge which one is the best. Luckily for us, someone has walked this path before and left this useful overview. Mockall has the greatest number of features and quick research reveals that it is the only framework that is still actively developed. So the choice is not so hard after all.

Let’s add a simple trait and see how we can mock it.

pub trait HidingPlace {
    fn has_marmot(&self) -> bool;
}

pub fn find_marmot_in(hiding_place: &dyn HidingPlace) -> bool {
    return hiding_place.has_marmot();
}

Following documentation I just do this:

use mockall::*;
use mockall::predicate::*;

#[automock]
pub trait HidingPlace {
    fn has_marmot(&self) -> bool;
}

And it works. I can now use MockHidingPlace structure in my tests. However, I don’t feel comfortable with polluting my code with test specific statements, so I will try to move it into the dedicated module. More doc reading and I found a way to do this:

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

    mock! {
        pub Hole {}
        impl HidingPlace for Hole{
            fn has_marmot(&self) -> bool;
        }
    }

    #[test]
    fn when_search_deep_then_marmot_found() {
        let mut mock = MockHole::new();
        mock.expect_has_marmot()
            .times(1)
            .returning(||true);
        assert_eq!(find_marmot(21, &mock), true);
    }
}

Summary

Setting up a unit test framework in Rust means adding specialized crates into your project. Each one brings single piece of functionality, like mocking or float asserts, and together they create a full testing environment. Not as perfect as solutions we know from C or C++ but good enough to test our code and move to the next chapter of our journey.



* In this blog post, Mozilla guys suggest using the standard test tool from Rust so maybe I just underestimate its potential

Journey with Rust Part 1: Errors everywhere

Working with Rust is an exciting adventure. One that starts quite grim: an evil compiler tries to cut your head off every time you write a single line of code. But as you move forward and obey few simple rules, things become better and less painful. Eventually, you master the language and reach a secret world of software perfection and beauty. And once you are there, you never want to go back…

That is what the Internet once told me, so I took my keyboard and my screen and started the journey on my own. First miles of code and I get attacked by errors from every possible direction.

Compilation errors

There is nothing more frustrating than being told what to do: teachers, parents, my wife, my boss… almost everyone. And now my compiler as well. “Consider removing this semicolon”. So I do. “Consider borrowing here”. So I do. Line after line just applying fixes until it finally shuts up and lets my program run.

Try to do this:

let mut array: [i32; 2] = [1, 2];
array[2] = 3;

and you get this:

error: this operation will panic at runtime

array[2] = 3;
^^^^^^^^ index out of bounds: the length is 2 but the index is 2

Try this:

let mut ARRAY: [i32; 2] = [1, 2];
ARRAY[1] = 3;

and you get this:

warning: variable `ARRAY` should have a snake case name

let mut ARRAY: [i32; 2] = [1, 2];
        ^^^^^ help: convert the identifier to snake case: `array`

Working with Rust compiler is an exceptional experience. You quickly start to understand, that the little gnome behind the screen* does not trust you at all. It checks every step you take, making sure you wont go off the beaten track. Very frustrating if you ever worked with C++ but this behavior was not added just to make your life miserable. There are some great benefits of working by the rules.

Memory safety (more compilation errors)

The biggest advantage of working with Rust is the memory safety. Forget about nullptr dereferencing, uninitialized variables, deadlocks, race conditions, and all those things that make software engineering such an “interesting” discipline. No more UFO bug tickets starting with “seen only once”. No more week-long investigations, no more running the same code for 1000th time, hoping that it will finally crash. Guess what will happen if I try to do this:

let i : i32;
println!("Here is some garbage: {}", i);

Exactly. The compiler gets angry and throws red errors at me. And it won’t stop until I agree to start coding as it wants (see point 1) and initialize variables before use. Let’s do something even more crazy and try spin multiple threads:

use std::thread;

fn main() {

    static mut NOT_PROTECED : i32 = 0;

    thread::spawn(|| {
        for _ in 0..10 {
            NOT_PROTECED += 1;
        }
    }).join().unwrap();

    println!("Val: {}", NOT_PROTECED);
}

Now the real battle begins. For every single fix there are two new errors. After one hour of pasting random stuff from Stack Overflow the compiler finally shuts up.

use std::thread;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;

fn main() {

    let protected = Arc::new(AtomicI32::new(0));
    let result = Arc::clone(&protected);
    thread::spawn(move || {
        for _ in 0..10 {
            let val = protected.load(Ordering::Relaxed);
            protected.store(val + 1, Ordering::Relaxed);
        }
    }).join().unwrap();

    println!("Val: {}", result.load(Ordering::Relaxed));
}

Our gnome is maybe grumpy but it just saved us from some concurrency issues and, what is more important: possible future problems that would definitely arise as the software gets older and bigger.

Some magic

This is a good place to introduce a powerful magic spell: an unsafe keyword which tells the compiler: “trust me I know what I am doing”. It enables shooting in your own foot mode which we know so well form C++. No limits, no errors. This will compile just fine.

fn main() {

    unsafe {
        static mut NOT_PROTECED: i32 = 0;

        thread::spawn(|| {
            for _ in 0..10 {
                NOT_PROTECED += 1;
            }
        }).join().unwrap();

        println!("Val: {}", NOT_PROTECED);
    }
}

Very tempting, but writing your program inside a big unsafe clause is not considered the best Rust practice. This word should be used with external, well tested libraries – things that we know won’t break and won’t introduce an undefined behavior. In short – code that is not written by us.

Error handling (back to compilation errors)

Do you want to be famous? Of course you do. And there is no better way to become famous than to deadlock a space orbiter or blow up a nuclear power plant.

We already know that Rust won’t allow us to inject any nasty race condition but how about reading from a non existing file?

use std::fs::File;
use std::io::prelude::*;

fn main() {
    let mut new_command = String::new();
    let mut file = File::open("important_space_orbiter_instructions.txt");
    file.read_to_string(&mut new_command);
    println!("Space station please do: {}", new_command);
}

This wont fly. And I mean the code not the space station. The return type of file create is a Result. Result can be something (file handler in our case) or it can be an error (if the operation failed). And the best part is: you can’t ignore the error condition (guess who will complain if you do). A simplest way to make our code compile, is to panic in the case of failure. The expect keyword below will make our program terminate if the open won’t succeed.

fn main() {
    let mut new_command = String::new();
    let mut file = File::open("important_space_orbiter_instructions.txt").expect("Can't read file");
    if file.read_to_string(&mut new_command).is_ok() {
        println!("Space station please do: {}", new_command)
    }
}

But the real power of Result type comes with the “?” operator, which simply means – in case of error return error.

use std::fs::File;
use std::io::prelude::*;

fn read_command() -> std::io::Result<String> {
    let mut new_command = String::new();
    let mut file = File::open("important_space_orbiter_instructions.txt")?;
    file.read_to_string(&mut new_command)?;
    Ok(new_command)
}

fn main() {
    let command = read_command().expect("Can't get command")
    println!("Space station please do: {}", command);
}

This way the error handling logic does not pollute the normal execution path. Everything is nice, clean and safe. Thank you little gnome.

Summary

When working with Rust, you can get a feeling that the compiler is working against you and you need to fight it every time you want to have something done. But think about it this way: when going on a war, who would you prefer to be on your side? A grumpy guy who will point out every little mistake you make or a silent fellow that tells nothing even when you hold your rifle by the wrong end.

Last word

One important note to know when you start programming with Rust: assignment is a move operation.

*Compiler Gnomes – small creatures living On The Hardware Side. With their tiny binary axes they chop human readable text into pieces that are used to prepare a binary soup**

**Binary soup – soup that tells your computer what to do. Tastes like oranges