Artisanal Mocks: Locally Sourced Organic Unit Tests!


#1

tldr: conditional #[cfg(test)] can be used as a homegrown solution for mocking clients that call to other services.

I am trying to convince my team at work to write one of our webservices in Rust. I have developed a proof of concept that is cleanly written and works as expected. However, at my company (and hopefully all companies) unit tests are a requirement for production grade software.

“Unit tests are easy!” I thought to myself, thanks to Rust’s type system I already have all information about the structs and methods I need to work with, all I need to do is mock out this database call…

Suddenly, I realized that I had never actually mocked anything out in Rust. My first approach was to search for existing mocking solutions, and I came across two libraries for mocking, that may have promise in the future, but do not suit my current needs. Unfortunately, they seem to work only with traits, and have several limitations (how do I see the call history for methods I am mocking, etc?).

I am writing this in case it can help anyone that is dealing with the same problems I was.

In order to explain how I came to my solution (which may not be the best, but it works for me), let us consider the following struct that represents an incoming request to search for people in a geographic region based on several criteria. It will search a database and then take the results and map them to a more friendly format:

use clients::db_client;

pub struct ExecutableRequest {
    pub address: Address,
    pub gender: Gender
}

impl ExecutableRequest {
    fn execute(&self) -> Result<PrettySearchRecords, ()> {
        match db_client::find_people(self.address, self.gender) { // how do we mock this call in unit test?
            Err(_) => Err(()),
            Ok(raw_search_results) => Ok(map_to_pretty_search_format(raw_search_results))             
        }
    }
}

If I were to unit test this purposefully simple example, I would want to make sure that given a successful db response, I would want to make sure that execute returns some expected mapped object.

The simplest approach is probably to use trait objects, and simply inject it into the execute method as a parameter such as:

fn execute(&self, db_client: &DbClient) 

That way in our test we can simply create a dummy struct that implements the DbClient trait, and then use it as desired. However, this implies a performance penalty due to dynamic dispatch and forces me to add a parameter to a calling method. If I only have one db_client that I will ever use, why do I need to pass it into the method every time?

Another way is to use generics. When we are running the program normally, we can instantiate the struct with the concrete db client generic impl, and when running in a test environment, we can instantiate it with a mock impl. This was the first approach I tried, but I did not like the complexity that introducing generics created, especially since the generic solution was not needed by the program and only existed to serve the unit tests.

Before continuing, I changed my executable struct to the following. Notice how I am no longer using db_client as a module with standalone functions, I have created a unit struct (with no fields) called DbClient that I am placing inside my request.

struct DbClient;
impl DbClient {
    pub fn new() -> Self {
        DbClient
    }

    pub find_people(&self, address: Address, gender: Gender) -> DbResult {}
}
pub struct ExecutableRequest {
    pub address: Address,
    pub gender: Gender,
    pub client: DbClient
}

When executing the call, I simply use self.client.find_people.

I ultimately decided to use conditional compilation to my advantage in order to avoid using generics. Recall the db_client from the earlier code. To make my code testable, I modified the clients/mod.rs file to the following:

pub mod db_client {
    #[cfg(not(test))]
    pub use super::db_client_concrete::*;

    #[cfg(test)]
    pub use tests::mocks::db_client::*;
}

That way, when I run unit tests, I can guarantee myself that a mock implementation will be used. However, this still does not solve the issue of viewing the client’s call history, as well as forcing the client to return expected_results for testing purposes. This is where the artisanal mocking comes into play. Unfortunately, this is very adhoc and requires a custom implementation for every client I am mocking, which is slightly annoying, but ultimately not a serious issue:

// changed from a unit struct to contain testing information about how it is called
struct DbClient {
    pub expected_result: DbResult,
    pub call_history: RefCell<Option<CallArgs>>
}

pub struct CallArgs {
    pub address: Address,
    pub gender: Gender
}

impl DbClient {
    // notice the new impl has changed
    pub fn new() -> Self {
        DbClient {
            should_fail: false,
            call_history: None
        }
    }

    // new method added to client in order to get the call history after execute is returned
    pub fn get_call_history(&self) -> CallArgs {
        let call_history = self.call_history.clone();
        let call_history = call_history.borrow();
        call_history.clone().unwrap()
    }

    pub find_people(&self, address: Address, gender: Gender) -> DbResult {
        let call_args = CallArgs {
            address: address,
            gender: Gender
        };

        *self.call_history.borrow_mut() = Some(call_args);

        self.expected_result.clone()
    }
}

Now when I am performing unit testing, I am able to mock the returned response from the db client, as well as confirm that it was passed in the appropriate args.

I hope that this is helpful. It is certainly not perfect by any means, but I am posting it here since I was unable to find another solution that was comfortable for me.


Introducing mock_me crate for mocking/injecting functions during testing
#2

I also hand rolled unit tests and I also posted a thread about it.

The short version is I found using Rust’s trait bounded generics to be a reasonable solution, in contrast to C++ templates which I’d dread having to use so extensively.


#3

A combination of what you did in your post and what withoutboats is suggesting with default type parameters looks like it could be a good approach as well.


#4

I am the author of one of those mocking libraries. And what I am missing most is feedback. You must be the only user which actually tried (or not?) my library, and not just starred it :slight_smile:
So maybe you should express you needs, and I will try to implement them.

While my library requires you to specify trait, it seems not too high price for not having to implement all machinery manually. And you are not forced to use this trait at all for static or dynamic dispatch. Use it just to generate mock:

# client::concrete module

struct DbClient {
  fn foo(...) { ... }
}

# client::mock module

#[derive(Mock)]
trait DbClient {
  fn foo(...);
}

# top module

#[cfg(not(test))]
    pub use client::DbClient;

    #[cfg(test)]
    pub use client::mock::DbClientMock as DbClient;
}

And I can simplify using my library for such scenario. For example, I can use trait definition to just generate mock struct without actually requiring or implementing trait itself.


#5

I had thought that the concrete DbClient had to be a trait as well, so it is good to know that I was mistaken.


#6

I think you mean “concrete DbClient to implement trait as well”.

Well, it is still better for concrete struct to implement trait, because this way interfaces of your concrete and mock structs cannot diverge — you will get compiler error early, not only when compiling one specific configuration. And since in your approach you don’t use trait references, there will be no overhead, so why not.