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:
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.
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.
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.
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.
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.
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.
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 { // … } }
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.
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.
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 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) ); } }
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); }
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:
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); } }
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:
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,))); } }
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); } }
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() ); } }
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)); } }
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()); } }
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.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.