In this post we will continue with the development of a small Rust crate called FuzzyComp. In the first part we wrote some code and now we want to make sure it works. There are several levels at which we can test, from the entire application down to each individual component. This blog is about writing tests for a single function. Luckily for us, Rust already comes with a very powerful multi-threaded unit test engine that we can easily use out of the box.
Motivation
In my first blog post, we created a few functions to compare two floats. In this post, we are going to verify they work as intended. Unit tests play a dual role in the software development process. First, we verify that the function works as expected for a given set of inputs. Second, if we change or optimize the function in the future, we can verify that it still works as expected after the change. This second property will help us when we convert our functions into generic functions in the future.
Considerations
When we write unit tests, we think in terms of possible inputs and expected outputs. With that in mind, let’s ask ourselves a few questions:
- Can any argument be None? For C-like languages, can any argument be NULL?
- Is the function expected to fail on certain values of the arguments?
- Is the function expected to fail on a combination of arguments?
- Are there special values for outputs that we expect in some cases?
- What are the limits and how do we handle them? What happens when compares just succeed or just fail?
- If the function has branches, do all branches work?
- What are the main categories of inputs, in the case of numbers, how does our function handle negative numbers, positive numbers, and how does it respond to 0?
- If the error was ever detected and fixed in the original function, can we test for it?
Tests should answer all of the above questions. They are our way of proving that a function really does what we think it does.
When it comes to unit tests, it’s important that we never assume what will happen in the actual code. Unit tests focus on single units of code and therefore should force us to think defensively. If we work under the assumption that our code will be misused, we end up producing higher quality code and naturally catch bugs that might occur throughout the system before they can propagate to hard-to-debug places.
Code
Rust comes with a really nice and efficient unit testing system. We can write tests directly into the file with our functions, usually at the end.
#[cfg(test)]
mod tests {
}
This code creates a submodule inside our file. Since it’s marked as a “test”, Rust knows that it should only be built and included when we actually test the code. To test one of the functions we created earlier, we can include a test function within our submodule that looks something like the following.
#[test]
fn eq() {
assert!(super::eq(std::f64::consts::PI, 3.0, 0.2));
assert!(super::eq(9.1, 9.1, 0.0));
assert!(super::eq(8.4, 9.1, 1.0));
assert!(super::eq(-0.1, -0.3, 0.2));
assert!(super::eq(-0.1, 0.1, 0.2));
assert!(!super::eq(0.1, 0.2, 0.01));
}
This is an example of a test for our fuzzy equals function. If this function executes successfully (no assert! panics), the test is considered a success. The function itself is marked as a test and can be called individually if we only want to test that particular function. If we ever find a bug in one of our functions, we can simply add more asserts to make sure they do not happen again in the future.
Running tests
After writing the tests, we can navigate to our project folder from the command line. From there, we run:
cargo test
This will run all the tests and produce output that looks similar to the one below. (It also runs other types of tests, but those are not relevant to us right now, so we’ll ignore that part).
running 6 tests
test compare::tests::eq ... ok
test compare::tests::gt ... ok
test compare::tests::le ... ok
test compare::tests::ge ... ok
test compare::tests::lt ... ok
test compare::tests::ne ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
As we can see, all the tests for our mini library ran successfully. That is a big success. Now we are ready for the next step, making our little library generic.