DEV Community

Cover image for Mocking in Rust: Mockall and alternatives
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Mocking in Rust: Mockall and alternatives

Written by Oduah Chigozie✏️

Testing is an integral part of software engineering. For beginners, writing a test case makes sure that your code does exactly what you expect it to do. Every programming language has various frameworks that help you test your code.

Small pet projects can get away with not having testing in place, but as an application scales, you run into the risk of hitting a wall where you become paranoid after you push a new feature to production.

Some teams use a manual tester who performs regression testing. This is great in theory, but a manual tester cannot capture all the intricacies that arise during runtime. And, given the tools available for automated testing, it is expensive and unproductive to use a manual tester.

In spite of this, the percentage of engineers who prefer to test their code is very small; but if you look at the best engineering teams that build high-quality software, testing will be an integral part of their workflow.

In this article, we’ll investigate an important testing technique in Rust called mocking. Mocking in Rust involves replacing real dependencies with mock objects. These objects simulate the behavior of the real dependencies.

Mocking lets you isolate the unit under test by controlling the dependencies' behavior. Thereby, making your tests more focused and reliable.

In our deep dive into mocking in Rust, we’ll look at how mocking differs from general unit testing and demonstrate how to implement mocking using the Mockall library. Finally, we’ll evaluate several alternative libraries to consider for mocking in Rust.

Jump ahead:

What is unit testing?

Now that you know the importance of testing your code, let’s look at how unit testing works. Once you fully understand how unit testing works, you'll understand the need for mocking.

Let’s assume you have a function that takes in two numbers and returns the division of those two numbers.

function divide (a,b){
        return a / b
}

Pretty simple function. You provide two numbers, and you get an output; but, the question is, is that always the case?

What if b is 0? It produces Zero Division Error in most languages, since anything divided by zero is infinity.

What if a and b are arrays? Can you be sure that the code calling your function will only pass the expected data types? Unfortunately, you cant.

This is where unit testing comes in. Unit testing tests your code in many ways to make sure your code can handle these types of anomalies.

Well, it doesn’t do that automatically — you have to write those test cases yourself.

For example, to test the division function, you write some test cases as in the following:

expect divide(2,2) to be 1

expect divide (1,0) to throw an error

Now you know why developers usually don’t like to write test cases. It’s a lot of work, but once you get used to it, the benefits justify the effort.

What is mocking in unit testing?

Mocking is a practice in unit testing. In mocking, you create fake objects, called mocks, from real objects for testing purposes. These mocks simulate the behavior of the real object as much as it needs to.

You can only use mock objects for testing purposes. The goal of mocking is to isolate a unit of code that you want to test, away from its dependencies. Mocking helps you test the unit in isolation.

Performing unit tests in Rust is different from other programming languages. In many programming languages, you put all your tests in a dedicated test folder. In Rust, you put all your tests in your program file.

Let’s say you have written the code for a project. You’d write your tests at the bottom of your code in a mod test module. For example:

fn main() {
  task1();
  task2();
  println!("Accomplished tasks!");
}

fn task1() -> String {
  "Accomplished task 1!".to_string()
}

fn task2() -> String {
  "Accomplished task 2!".to_string()
}

mod test {
   // Import all functions to test module
   use super::*;

   #[test]
   fn task1_works() {
      assert_eq!(task1(), "Accomplished task 1!".to_string() );
   }

   #[test]
   fn task2_works() {
      assert_eq!(task3(), "Accomplished task 2!".to_string() );
   }
}

This is common practice in Rust.

Mocking vs. faking

Faking is the practice of creating simplified implementations of objects or services. Unlike mocks, fakes are functional implementations. Fakes behave like real objects or services, while mocks simulate their behavior. Fakes have a simpler and faster behavior, compared to real objects or services.

Mocking vs. stubbing

In stubbing, you replace a real object with a simplified or artificial version. The stub object can simulate the real object's behavior in a controlled manner. You can use a stub object to isolate the test code from its dependencies. Stubbing allows the test to focus on the specific functionality under test.

In other words, a stub is a test double that provides predefined responses to method calls. Like mock objects, stub objects allow the test to function without relying on the real implementation of the dependency. But they are simpler and used for simpler dependencies, while mocks are more complex and used for more complex dependencies.

Creating a Rust mock object with Mockall

Mockall is a library that provides tools to create mock objects. You inject the mock object in place of the real dependency into the unit under test. Using a mock object helps to verify how the unit behaves under different conditions.

Mockall provides both automatic and manual methods for creating mock objects from traits. We’ll demonstrate how to create a mock object using each of these methods.

Creating a mock object automatically with automock

To create a mock object automatically, use the #[automock] modifier on the trait that you want to mock.

Take a look at the example below:

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

#[automock]
trait MyTrait {
  fn foo(&self) -> u32;
  fn bar(&self, x: u32) -> u32;
}

let mut mock = MockMyTrait::new();

In this example, #[automock] generates a mock struct from MyTrait. The mock struct’s name starts with Mock, followed by the name of the trait. In the example, the last line initializes a mock object from the mock struct.

Creating a mock object automatically is the easiest method, but may not be suitable for creating more complex objects. In those cases, you may need to use a manual method.

Creating a mock object manually

To create a mock object manually, you’ll need to use the mock! macro. Take a look at the example below:

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

trait MyTrait {
  fn foo(&self) -> u32;
  fn bar(&self, x: u32) -> u32;
}

mock! {
  pub MyStruct {}

  impl MyTrait for MyStruct {
    fn foo(&self) -> u32;
    fn bar(&self, x: u32) -> u32;
  }
}

let mut mock = MockMyStruct::new();

In this example, the mock macro creates a MockMyStruct struct. MockMyStruct is the mock version of MyStruct.

With this method, you can install many traits to a mock struct; unlike the automatic method which limits you to just one trait per mock struct. Using this manual method, you can write a mock struct like this:

trait MyTrait1 {
  // …
}

trait MyTrait2 {
  // …
}

trait MyTrait3 {
  // …
}

mock! {
  pub MyStruct {}

  impl MyTrait1 for MyStruct {
    // …
  }

  impl MyTrait2 for MyStruct {
    // …
  }

  impl MyTrait3 for MyStruct {
    // …
  }
}

Modifying a mock object’s behavior

Creating the mock object isn't enough alone to mimic the behavior of a dependency. To test your mock object, each method needs to behave like the real dependency. Mockall allows you to set the behavior of each method in your mock object. Take a look at the example below:

let mut mock = MockMyTrait::new();

mock.expect_foo()
    .return_const(44u32);

mock.expect_bar()
    .with(predicate::ge(1))
    .returning(|x| x + 1);

In this code, we modified the behavior of the foo and bar methods in mock. We set foo to return 44 (as unsigned 32-bit integer) every time. Then, we set bar to take any number greater than or equal to 1 as an argument, and to return an increment of its argument.

You can look into other predicate functions and expectations to see more changes you can make to the mock object. You can also set an order in how to call each method with a sequence.

Testing mock structs and traits

Now that you have your mock object ready, it’s time to see how it runs. Take a look at the example below:

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

#[automock]
trait MyTrait {
  fn foo(&self) -> u32;
  fn bar(&self, x: u32) -> u32;
}

fn function_to_test(my_struct: &dyn MyTrait) -> u32 {
    my_struct.foo() + my_struct.bar(4)
}

fn main() {
  let mut mock = MockMyTrait::new();

  mock.expect_foo()
      .return_const(44u32);

  mock.expect_bar()
      .with(predicate::eq(4))
      .returning(|x| x + 1);

  assert_eq!(49, function_to_test(&mock));

  println!("All good!");
}

In this example, we are testing the function_to_test function. The function accepts any object that implements the MyTrait trait, including the mock object.

Alternatives to Mockall

Besides Mockall, there are other libraries for mocking in Rust. Exploring the alternatives can help you find the most suitable library for your project.

Let's look at a few of the alternatives.

Mockers

Mockers was inspired by the Google Mock library for C++. Mockers has an efficient syntax that supports stable Rust; however, you can only use some features in nightly Rust (like generic functions).

Mockers uses Scenario objects to create and control mock objects. Scenario objects allow you to create mock objects efficiently.

Here's an example of mocking with Mockers:

mod test {
   #[cfg(test)]
   use mockers::Scenario;

   #[cfg(test)]
   use mockers_derive::mocked;

   #[cfg_attr(test, mocked)]
   trait MyTrait {
       fn do_something(&self, x: i32) -> i32;
   }

   // Define a function that uses the trait
   fn my_function(obj: &dyn MyTrait, x: i32) -> i32 {
       obj.do_something(x)
   }

   // Write a test that uses the mock object
   #[test]
   fn test_my_function() {
       // Create a new mock object and scenario
       let scenario = Scenario::new();
       let (my_mock, my_mock_handle) = Scenario::create_mock_for::<dyn MyTrait>(&scenario);

       // Define the expected behavior of the mock object
       scenario.expect( my_mock_handle.do_something(10).and_return(42) );

       // Verify that the mock object was called as expected
       assert_eq!(42, my_function(&my_mock, 10) );
   }

}
Enter fullscreen mode Exit fullscreen mode

Mock Derive

Mock Derive is useful in simplifying the mocking process. It lets you set up unit tests even when using another testing system, like cargo test.

Mock Derive doesn’t have a stable release yet. At the time of writing, it is still under development and may not support several real world use cases. Regardless, here’s an example of mocking with Mock Derive:

use mock_derive::mock;

// Define a trait that we want to mock
#[mock]
trait MyTrait {
   fn do_something(&self) -> i32;
}

// Write a test that uses the mock object
#[test]
fn test_my_function() {
   // Create a new instance of the mock object
   let mut mock = MockMyTrait::new();

   // Set expectations on the mock object
   mock.method_do_something()
       .first_call()
       .set_result(32);

   // Inject the mock object into the function under test
   let result = mock.do_something();

   // Verify that the mock object was called as expected
   // mock.assert();
   assert_eq!(result, 32);
}
Enter fullscreen mode Exit fullscreen mode

Galvanic Mock

Galvanic-mock is a behavior-driven mocking library. It is part of the testing libraries that work with galvanic-test and galvanic-assert.

Galvanic Mock allows you to achieve the following tasks:

  • Create a mock object from one or more traits
  • Define behaviors for mock objects based on patterns
  • State expectations for interactions with mocks
  • Mock generic traits and traits with related types
  • Mock generic trait methods
  • Integrate your tests with galvanic-test and galvanic-assert

Here's an example of mocking with Galvanic Mock:

// `galvanic_mock` requires nightly Rust
extern crate galvanic_mock;
use galvanic_mock::{mockable, use_mocks};

#[mockable]
trait MyTrait {
   fn do_something(&self, x: i32) -> i32;
}

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

   #[test]
   #[use_mocks]
   fn simple_mock_usage() {
       // create a new object
       let mock = new_mock!(MyTrait);

       // define behaviors for the mock object
       given! {
           <mock as MyTrait>::do_something( |&x| x < 0 ) then_return_from |&(x,)| x - 1 always;
           <mock as MyTrait>::do_something( |&x| x > 0 ) then_return_from |&(x,)| x + 1 always;
           <mock as MyTrait>::do_something( |&x| x == 0 ) then_return 0 always;
       }

       // matches first behaviour
       assert_eq!(mock.do_something(4), 5);

       // matches second behaviour
       assert_eq!(mock.do_something(-1), -2);

       // matches last behaviour
       assert_eq!(mock.do_something(0), 0);

   }
}
Enter fullscreen mode Exit fullscreen mode

Pseudo

Pseudo is a small mocking library. It provides exactly what you need for mocking, and nothing more. Here are some things you can do with Pseudo:

  • Mock trait implementations
  • Track function call arguments
  • Set return values
  • Override functions at test time

Some libraries mentioned in this section have unstable features. Unstable features are the reason some libraries only work in nightly Rust.

Here’s an example of mocking with Pseudo:

extern crate pseudo;

use pseudo::Mock;

// define the trait we want to mock
trait MyTrait: Clone {
   fn do_something(&self, x: i32) -> i32;
}

// use the trait to create a mock struct
#[derive(Clone)]
struct MockMyTrait {
   pub do_something: Mock<(i32,), i32>,
}

// implement the trait for the mock struct
impl MyTrait for MockMyTrait {
   fn do_something(&self, x: i32) -> i32 {
       self.do_something.call((x,))
   }
}

fn function_to_test <T: MyTrait> (my_trait: &T, x: i32) -> i32 {
   my_trait.do_something(x)
}

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

   #[test]
   fn doubles_return_value() {
       let mock = MockMyTrait { do_something: Mock::default() };

       mock.do_something.return_value(2);

       // test `function_to_test`
       assert_eq!(function_to_test(&mock, 1), 2);
   }

   #[test]
   fn uses_correct_args() {
       let mock = MockMyTrait { do_something: Mock::default() };

       assert!(!mock.do_something.called());

       function_to_test(&mock, 1);

       assert_eq!(mock.do_something.num_calls(), 1);
       assert!(mock.do_something.called_with((1,)));
   }
}
Enter fullscreen mode Exit fullscreen mode

Wiremock

Wiremock provides mocking services for applications that interact with HTTP APIs. With Wiremock, you can create mock HTTP servers for testing.

Wiremock mocks HTTP responses using request matching and response templating techniques. Request matching checks if the incoming request meets specified conditions. You specify these conditions in the handler. Response templating helps to generate the content of the API response.

Here's an example of mocking with Wiremock:

#[cfg(test)]
mod test {
    use wiremock::{MockServer, Mock, ResponseTemplate};
    use wiremock::matchers::{method, path};

    #[tokio::main]
    #[test]
    async fn hello() {
        // Start a mock HTTP server on a random port locally.
        let mock_server = MockServer::start().await;

        // Set up the mock server's behavior.
        Mock::given(method("GET"))
            .and(path("/hello"))
            .respond_with(ResponseTemplate::new(200))   // Respond with 200 status when it receives a GET request on '/hello'.
            .mount(&mock_server)  // Mount the behaviour on the mock server.
            .await;

        // Test the mock server with any HTTP client to see if it behaves as expected.
        let status = surf::get(format!("{}/hello", &mock_server.uri()))
            .await
            .unwrap()
            .status();
        assert_eq!(status as u16, 200);
    }

    #[tokio::main]
    #[test]
    async fn missing_route_returns_404() {
        // Start a mock HTTP server on a random port locally.
        let mock_server = MockServer::start().await;

        // Set up the mock server's behavior.
        Mock::given(method("GET"))
            .and(path("/hello"))
            .respond_with(ResponseTemplate::new(200))   // Respond with 200 status when it receives a GET request on '/hello'.
            .mount(&mock_server)  // Mount the behaviour on the mock server.
            .await;

        // Test the mock server for nonregistered routes. It returns status 404 as expected. 
        let status = surf::get(format!("{}/missing", &mock_server.uri()))
            .await
            .unwrap()
            .status();

        assert_eq!(status as u16, 404);
    }
}
Enter fullscreen mode Exit fullscreen mode

Faux

Faux allows you to create mock versions of a struct without complicating your code. Like many mocking libraries, faux is only recommended for testing purposes. Mock objects in production may be unstable and cause production problems.

The Faux library only mocks a struct's public methods. The library doesn't mock any private methods or fields. Only mocking public methods keeps the mock objects as large as it needs to be.

Here's an example of mocking with Faux:

// `faux::create` makes `MyStruct` mockable
#[cfg(test)]
#[faux::create]
pub struct MyStruct { }

// `faux::methods` makes all public methods of `MyStruct` mockable
#[cfg(test)]
#[faux::methods]
impl MyStruct {
   pub fn do_something(&self, x: usize) -> String {
       "Result of doing something".to_string()
   }
}

mod test {
   use super::*;

   #[test]
   fn it_works() {
       // create mock version of MyStruct with `faux` method
       let mut mock = MyStruct::faux();

       // mock fetch only if the argument is 3
       faux::when!(mock.do_something(3)) // argument matchers are optional
           .then_return( "A third string".to_string() );  // stub the return value for this mock

       assert_eq!(mock.do_something(3), "A third string".to_string() );
   }
}
Enter fullscreen mode Exit fullscreen mode

Unimock

Unimock is a different type of mocking library. Unlike other libraries, Unimock implements all generated mock objects with the same type. Compared to other libraries, this method has better flexibility and efficiency in tests.

Let’s take a look at an example to see how mocking works with Unimock:

use unimock::{MockFn, matching, Unimock, unimock};

// Create a mock version of `MyTrait`
#[unimock(api=MockMyTrait)]
trait MyTrait {
   fn do_something(&self) -> i32;
}

// Write the function to test
fn test_me(mock: impl MyTrait) -> i32 {
   mock.do_something()
}

#[cfg(test)]
mod tests {

   use super::*;

   #[test]
   fn test_function_works() {
       // Program a behavior for `MockMyTrait.do_something`
       let clause = MockMyTrait::do_something
           .each_call(matching!())
           .returns(1337);

       // Initialize the mock object
       let mock = Unimock::new(clause);

       assert_eq!(1337, test_me(mock));
   }
}
Enter fullscreen mode Exit fullscreen mode

Mry

Mry allows you to easily create mock objects for unit testing. You can integrate Mry with any testing framework for Rust. Including the inbuilt testing framework, cargo test.

Mry is an easy-to-use library. It provides an easy API to construct mock objects. Here's an example of mocking with Mry:

// Create a mockable struct `MyStruct`
#[mry::mry]
struct MyStruct {}

#[mry::mry]
impl MyStruct {
   fn do_something(&self, count: usize) -> String {
       format!("The trait says {}", count)
   }
}

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

   #[test]
   fn meow_returns() {
       // Initialize a mock object from `MyStruct`
       let mut mock = mry::new!( MyStruct{} );

       // Construct a behavior for the mock object
       mock.mock_do_something(mry::Any)
           .returns( "Called".to_string() );

       // Test the mock object's behaviour
       assert_eq!(mock.do_something(2), "Called".to_string());
   }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Mocking allows developers to isolate a unit for testing. You can control the behavior of the unit's dependencies, and make the test more focused. Mocking is particularly useful when dealing with complex systems or external dependencies. Especially cases where it is difficult to control the behavior of those dependencies.

In this article, we demonstrated how to create and use mock objects in Rust with Mockall, investigated how to change a mock object's behavior, and evaluated alternative mocking libraries.


LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket Dashboard Free Trial Banner

LogRocket is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — start monitoring for free.

Top comments (1)

Collapse
 
irfanghat profile image
Irfan Ghat

This is some great info. Will definitely be trying this out!