How to organize tests in Rust?



rust

Recently I begun to play with Rust - a promising, multi paradigm programming language, that looks similar to C family at first glance, but turns out to be something way more interesting.

At the same time I was obsessed with TDD (test-driven development) methodology and wanted to write used it in a Rust project. However, I encountered some problems with organizing the test directories in my Rust. We'll be investigating a project created with cargo, which is a Rust package manager integrated with compiler and testing tool.

How can we test our project?

There are three main types of tests in Rust:


  • unit tests

  • integration tests

  • documentation tests

Unit tests are used to check the functionality of functions and methods. They tend to be as independent as it's possible, so you'd want to test only one thing at time and nothing more.
In contrary integration tests are more holistic and their purpose is to check how everything works together. The last type of tests - documentation tests - are used to check whether examples included in your docstrings actually works. I guess it's a common problem to include an example of code in the docstring and later forget to update it when the code has changed.

First of all: where to put tests?


Rust documentation tells us to use a convention and store unit tests in the same file as the the tested code. To be honest, I had some doubts, because I used to put all tests in separate directory (for example in Python) with reasonably named files, for example: test_some_functionality.rs. Dropping code along with lots of tests in one file may seem messy, but on second thought it's a nice idea, because you


  • don't have to look for tests somewhere in the project dir

  • there is greater chance you'll write tests even if you are lazy

  • will have to keep your code more organized to prevent your files from overgrowing

Let's follow the standard then.

The integration tests should be placed in the separate directory tests in the project root. This is where cargo will be looking for our test files.

Unit tests

First I'll create empty cargo project and add some code.

1 cargo new rust_unittests

Structure of my project is following:

1 .
2 ├── Cargo.lock
3 ├── Cargo.toml
4 ├── src
5 │ ├── lib.rs
6 │ └── main.rs
7 │ └── functions.rs
8 └── tests
9 └── test_functions.rs

Contents of Cargo.toml:

1 [package]
2 name = "my_unittests"
3 version = "0.1.0"
4 authors = ["sireliah"]

Nothing really complicated at this point. Let's see the code:

 1 /// src/functions.rs
2
3
4 pub fn add(a: i32, b: i32) -> i32 {
5 /// Add two signed integers.
6 return a + b;
7 }
8
9
10 /// This marker tells the compiler to compile module below only when running tests.
11 #[cfg(test)]
12 mod test {
13
14 /// We need to use "use" declaration, because any other module is unaware of our "functions" module by default.
15 use functions;
16 /// Add
17 #[test]
18 fn test_add_positive_result() {
19 assert_eq!(2, functions::add(1, 1));
20 }
21
22 #[test]
23 fn test_add_negative_result() {
24 assert_eq!(0, functions::add(-1, 1));
25 }
26
27 #[test]
28 #[should_panic] /// This means that we expect this test to fail (raise panic), so it will pass.
29 fn test_add_panic() {
30 /// This one should fail.
31 assert_eq!(3, functions::add(1, 1));
32 }
33
34 }

As you can see, I have created a public function add(), which adds two integers. Below, I added module tests for unit tests. I used #[cfg(test)] annotation to inform the cargo to compile this module only when using command:

1 cargo test

All three are passing as you can see:

1 Running target/debug/deps/unittests-50c47774a39aa150
2
3 running 3 tests
4 test functions::test::test_add_negative_result ... ok
5 test functions::test::test_add_positive_result ... ok
6 test functions::test::test_add_panic ... ok
7
8 test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured


Integration tests

But what do do when you would like to have integration tests in different file and directory? That was problematic for me, because I wasn't really sure how importing libraries works in Rust. When to use extern crate functions and when mod functions?

Most important thing when using integration tests is to make sure that the cargo is able to determine the location of our library. Note that "tests" directory is treated as separate crate and is completely unaware of our library ("my_unittests" in my case). Hence, we should inform cargo where to look for the source.

I have created a function that should be tested by integration tests, because it uses another function as dependency.

 1 /// src/functions.rs
2
3 pub fn multiply(a: i32, number_times: i32) -> i32 {
4
5 /// Implementation of multiplication using addition.
6 let mut sum: i32 = 0;
7 for _ in 0..number_times {
8 sum = add(sum, a);
9 }
10 return sum;
11 }

Here's my unit test file:

 1 /// tests/test_functions.rs
2
3 extern crate my_unittests;
4
5 #[cfg(test)]
6 mod integration_tests {
7
8 use my_unittests::functions;
9
10 #[test]
11 fn test_multiply1() {
12 assert_eq!(4, functions::multiply(2, 2));
13 }
14
15 #[test]
16 fn test_multiply2() {
17 assert_eq!(100, functions::multiply(10, 10));
18 }
19
20 }

Note that I'm using extern crate my_unittests on the top of the file and use my_unittests::functions; inside the test module. First one includes whole my_unittests library and the second takes only "functions" from the namespace. But is it enough to run? Nope.

1 error[E0432]: unresolved import `my_unittests::functions`
2 --> tests/test_functions.rs:7:9
3 |
4 7 | use my_unittests::functions;
5 | ^^^^^^^^^^^^^^^^^^^^^^^ no `functions` in the root

"Functions" is a module in the Rust sense, but it wasn't included in the root of our library. By default all functions and modules in Rust are private, so we need put some effort to make them public. Let's add a line in src/lib.rs, which is a special place in the cargo project, as its content determines what belongs to the root.

1 /// src/lib.rs
2 pub mod functions;

By adding this declaration, we are telling the compiler that the "function" module (remember that a separate file is a separate module, right?) should be accesible from anywhere.

I must confess that it wasn't clear for me how to design structure of a Rust project and have test working at the same time. I hope this tutorial will be useful for you and you won't need to experiment too much with crates and modules.

Sources I used:

Rust documentation:
https://doc.rust-lang.org/book/crates-and-modules.html
https://doc.rust-lang.org/book/testing.html
https://rust-lang.github.io/book/second-edition/ch11-03-test-organization.html

Rust by example:
http://rustbyexample.com/meta/test.html

Crates documentation
http://doc.crates.io/guide.html#tests