Organizing unit tests in Rust

Published

How to organize unit tests in a relatively nice way in Rust? Unfortunately there is no clean solution, but it is possible to achieve a structure that is production worthy.

There are few things about Rust that made me double think whether I really want to use this - otherwise awesome - language for my projects, and one of those annoyances are unit tests, or more precisely, the way how they were intended to be organized within projects.

The official recommendation

Let's start with the intended usage of the test framework as per The Book.

The built-in testing framework differentiates between unit tests and integration tests. For the examples below let's use the following Cargo.toml:

[package]
name = "example"
version = "0.1.0"
edition = "2021"

Place the production code and the unit tests in lib.rs:

// src/lib.rs

pub fn add(a: i32, b: i32) -> i32
{
    a + b
}

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

    #[test]
    fn adding_numbers_returns_correct_sum()
    {
        assert_eq!(5, add(2, 3));
    }
}

Place the integration tests in a separate tests directory:

// tests/lib_test.rs

use example::add;

#[test]
fn adding_numbers_returns_correct_sum()
{
    assert_eq!(5, add(2, 3));
}

So what's the problem?

Well, the biggest issue is having both the production code and the unit tests within the same source file. Someone who came up with this design clearly wasn't into unit testing. But it would be fine, if there was a way to override the default functionality with some fancy config values in Cargo.toml. But no, at the time of writing this article, there isn't any.

When you write proper unit tests thinking of edge cases and typical usage scenarios, the amount of your test code would exceed the amount of your production code by a lot. So you would end up with unnecessarily long source files, where most of the content are just the unit tests.

But I'll go further: what if your workflow is TDD? Would you really jump up and down in your code every single time you change something? Come on.

What is the solution then?

After reading numerous articles, the closest thing to an organized, clean structure I could come up with was to just put the test files right next to the production source files and make sure they are in the same module.

// src/adder/mod.rs

pub mod adder;

#[cfg(test)]
pub mod adder_test;
// src/lib.rs

pub mod adder;
// src/adder/adder.rs

pub fn add(a: i32, b: i32) -> i32
{
    a + b
}
// src/adder/adder_test.rs

use super::adder::add;

#[test]
fn adding_numbers_returns_correct_sum()
{
    assert_eq!(5, add(2, 3));
}

Unfortunately there is no way to place the unit tests outside of the src directory, but separating the test files from the production source files already provides a considerable flexibility.

A highly discouraged approach

Before concluding the article, I wanted to say a few words about this dirty solution: since the integration tests have a very nice structure; basically almost what unit tests should look like, it could be tempting to just write "integration tests", and pretend they are unit tests.

There are however a few reasons why I would discourage this:

  • Semantics. If you write your unit tests as integration tests, well, then they are going to be treated as integration tests, which is not nice.
  • The output will become tedious, because each source file within the tests directory will produce separate sections of results.
  • Each source file within the tests directory will be compiled into a separate crate. This could hinder performance, especially in larger projects, although this is yet to be benchmarked.

Conclusion

Although Rust doesn't have a way to override the unit testing behavior via configuration and there aren't even signs of introducing such changes in the near future, it is still possible to organize your tests as long as the components of your library are also organized well.