Rust unit testing: mocking library
Using libraries for mocking

In earlier articles about test doubles, I showed how to create them without using any libraries. While I believe that is the best way to understand how they work, how they can help, and how to use them, it is far from being a convenient way of writing your tests.
In this article, I will explain how to use a mocking library and replace all the test doubles we wrote with others that are produced by the library. Prepare for a more streamlined version of the code that achieves the same goals with less effort!
Create a Megaweapon mock with mockall
Working for a supervillain is indeed challenging. You must be very productive if you don't want to mark yourself for
workforce elimination, ahem, reduction. Hence, writing your test doubles from scratch doesn't seem to be a smart
move. What then? Well, let me introduce you to the mocking libraries. Basically, they implement the same
functionality we did manually in a more reusable manner. And don't be afraid: they can be used to create all your test
doubles, not just mocks.
One of the most popular mocking libraries for Rust is mockall. It provides an ergonomic and robust API that leverages procedural macros to produce, control, and verify test doubles. Let's use it to replace the manually written test doubles and simplify the code.
The first test double that we will replace is the Megaweapon. But first, we need to add mockall to the project
using cargo. Since mockall is a dependency that we only want to include for our tests, not our production code, we
specify it as a development dependency with the corresponding option.
cargo add --dev mockall
Then, to replace the manual test double we created in supervillain.rs with one produced and controlled by mockall,
we need to import it. But we only want mockall imported when the code is compiled for testing.
#[cfg(test)]
use mockall::automock;
mockall produces test doubles automatically just using the automock attribute. And again, we only want this to
happen when the code is compiled for testing, so we use a conditional attribute that we add to the Megaweapon trait.
#[cfg_attr(test, automock)]
pub trait Megaweapon {
There are two tests that use WeaponDouble: non_intensive_attack_shoots_weapon_once() and
intensive_attack_shoots_weapon_twice_or_more(). Let's start with the first one.
In the non_intensive_attack_shoots_weapon_once() test, we modify the creation of the test double. Instead of using
WeaponDouble, our manually written test double, we use MockMegaweapon –"Mock" + trait name– that is the type
produced by the automock attribute.
let mut weapon = MockMegaweapon::new();
Now that we have replaced our manual test double with the mockall one, we instruct it to expect its shot() method to
be invoked only once and to return nothing. It is important that we do this at the beginning, in the arrangement part
of the test, before we use it.
weapon.expect_shoot().once().return_const(());
As you can see, the automock attribute macro applied to a trait –Megaweapon in this case– defines a new type with
the same name preceded by "Mock" and generates a method for each of the existing ones in the trait, but preceded by
"expect_". We can use those methods to declare your expectations. We don't need to add any assertions at the end of
the test, because the verification of those expectations is done automatically at the end of the test scope. mockall
uses the drop trait to perform the verification, as we did in our previous work.
Running the tests with cargo t --lib will show us that we have successfully replaced the test double in the first
test. Now we can do the same with the second one, as shown below. We could have omitted the call count expectation
times(), and it should still work. However, it would also pass if the supervillain shoots just once or 10 times
instead of the expected two or three.
#[test_context(Context)]
#[test]
fn intensive_attack_shoots_weapon_twice_or_more(ctx: &mut Context) {
let mut weapon = MockMegaweapon::new();
weapon.expect_shoot().times(2..=3).return_const(());
ctx.sut.attack(&weapon, true);
}
There is no need to keep the WeaponDouble or its implementation blocks, so we can remove them. We can also get rid of
the functions once() and at_least() that we created for these two tests.
With all these changes in place, we run the tests again with cargo t --lib and celebrate our first mocktail party 🍸.
Convert the stub
Using mockall has already simplified our tests. At least the ones that were using the Megaweapon. Our next scenario would
be to convert the stubbing part of the sidekick to use mockall, too. In this case, we are not working with a trait,
but with a concrete type.
In sidekick.rs, we import mockall for tests, as we did in the previous section.
#[cfg(test)]
use mockall::automock;
Then, we add a macro to the implementation block of Sidekick, so a mock is automatically produced when we compile
for testing.
#[cfg_attr(test, automock)]
impl<'a> Sidekick<'a> {
This should have taken us a step closer, but when we compile to check that everything goes fine, the compiler complains
about numerous errors related to lifetimes. Thankfully, there is another way to create a mock with mockall. It is
known as manual mock1 and uses the mock! attribute, and it provides us with more control over the mock that is
produced. We start by replacing the import in sidekick.rs and removing the conditional automock attribute of the
implementation block.
#[cfg(test)]
use mockall::mock;
Now, we can create the mock manually by declaring the methods that we need to mock also in sidekick.rs. Notice that
get_weak_targets() has been modified slightly to simplify the mocking, and new() hasn't been mocked.
#[cfg(test)]
mock! {
pub Sidekick<'a> {
pub fn agree(&self) -> bool;
pub fn get_weak_targets(&self, _gadget: &'a dyn Gadget) -> Vec<String>;
pub fn tell(&self, _ciphered_msg: &str);
}
}Providing a different version of a concrete type solely for testing requires using another crate that performs the modifications to the import paths for the tests, using different namespaces for the original type and the test double.
cargo add --dev mockall_double
This crate provides us with the double macro that changes the namespace when importing. We use it in
supervillain.rs and remove the imports of Sidekick, both for testing and regular use.
#[cfg(test)]
use mockall_double::double;
#[cfg_attr(test, double)]
use crate::sidekick::Sidekick;
Here, I would also like to transform the tests progressively, but mockall_double is an all-or-nothing change. So, we
comment out all but the tests that use the Sidekick test double. That means that we temporarily disable
fire_sidekick_if_doesnt_agree_with_conspiracy(), world_domination_stage1_builds_hq_in_first_weak_target(), and
tell_plans_sends_ciphered_message() and we work on keep_sidekick_if_agrees_with_conspiracy().
In the test that we haven't commented out, we modify the creation of the mock instance using the original type and the
constructor produced by the attribute. We declare the expectations using the same format as before, but in this case,
we use return_const() to provide the desired response to that invocation.
fn keep_sidekick_if_agrees_with_conspiracy(ctx: &mut Context) {
let mut mock_sidekick = Sidekick::new();
mock_sidekick.expect_agree().once().return_const(true);
ctx.sut.sidekick = Some(mock_sidekick);
We run the tests and get an error about Sidekick (double) not implementing Debug. So we derive the Debug trait in
the mock declaration.
#[cfg(test)]
mock! {
#[derive(Debug)]
pub Sidekick<'a> {
pub fn agree(&self) -> bool;
pub fn get_weak_targets(&self, _gadget: &'a dyn Gadget) -> Vec<String>;
pub fn tell(&self, _ciphered_msg: &str);
}
}
This time, when we run the tests again with cargo t --lib, this one should pass. Let's uncomment the second one, make
similar changes to it, and verify that it passes.
fn fire_sidekick_if_doesnt_agree_with_conspiracy(ctx: &mut Context) {
let mut mock_sidekick = Sidekick::new();
mock_sidekick.expect_agree().once().return_const(false);
ctx.sut.sidekick = Some(mock_sidekick);
Let's go with the third. In this case, we are using the mock as a stub, but we are also verifying that the method
get_weak_targets() is invoked. We also use returning() instead of return_const() because the returned value
depends on the execution of a closure. We leave the gadget and henchman test doubles as they are for now.
fn world_domination_stage1_builds_hq_in_first_weak_target(ctx: &mut Context) {
let gdummy = GadgetDummy {};
let mut hm_spy = HenchmanDouble::default();
let mut mock_sidekick = Sidekick::new();
mock_sidekick
.expect_get_weak_targets()
.once()
.returning(|_| test_common::TARGETS.map(String::from).to_vec());
ctx.sut.sidekick = Some(mock_sidekick);
The last test is a little bit different. We want to verify that the method is invoked with the right argument. We
import the predicate for checking the value of the argument (use mockall::predicate::eq;).
fn tell_plans_sends_ciphered_message(ctx: &mut Context) {
let mut mock_sidekick = Sidekick::new();
mock_sidekick
.expect_tell()
.with(eq(String::from(test_common::MAIN_CIPHERED_MESSAGE)))
.once()
.return_const(());
ctx.sut.sidekick = Some(mock_sidekick);
Now that we have fully replaced our Sidekick test double, we can delete it, i.e., the full doubles module. That
is some relevant simplification of our code. It seems that this effort to use mockall is paying off. Yay!
Run the tests with cargo t --lib and take pride in the results.
Replace the dummy with mockall
We had replaced –and simplified a lot– the Sidekick test double. Let's now substitute the GadgetDummy test
double. Remember that the dummies don't require any particular functionality. When a function or method takes an
instance of a type, but it is not used in the code are testing, we use a dummy. This is going to be very easy using
what we have already learned about mockall.
We first import the automock macro in gadget.rs.
#[cfg(test)]
use mockall::automock;
We enable auto-mocking for the Gadget.
#[cfg_attr(test, automock)]
pub trait Gadget: Send {
We replace GadgetDummy in the only test where it is used. We don't need to declare any expectations because it is a
dummy. Having an instance that we can use is good enough for the tests.
fn world_domination_stage1_builds_hq_in_first_weak_target(ctx: &mut Context) {
let gdummy = MockGadget::new();
But the compiler cannot find MockGadget. It has been created in the gadget module, and we haven't imported it yet.
Let's import it into the test module.
use crate::gadget::MockGadget;
We can delete the GadgetDummy and run the tests. They all pass.
A spy with some non-trivial logic
The next target is to convert the Henchman test double. This is used in two tests:
world_domination_stage1_builds_hq_in_first_weak_target() and
world_domination_stage2_tells_henchman_to_do_hard_things_and_fight_with_enemies().
Again, we import automock in henchman.rs.
#[cfg(test)]
use mockall::automock;
The Henchman can be auto-mocked using the automock attribute.
#[cfg_attr(test, automock)]
pub trait Henchman {
As we have learned in the previous section, we need to import MockHenchman into the tests.
use crate::henchman::MockHenchman;
In the first test using a henchman test double, we would like to verify that they are instructed to build the secret
headquarters at a specified location. This is very similar to the previous cases, but we need to check that its method
is invoked with the right argument. The function with() is used to check the value of the arguments used in the
method. with() is used in conjunction with a predicate to implement various comparisons. In this case, we just want
the argument to be exactly equal to the name of the first (location) target. Let's use the mock and write the
expectation.
let mut mock_henchman = MockHenchman::new();
mock_henchman
.expect_build_secret_hq()
.with(eq(String::from(test_common::FIRST_TARGET)))
.return_const(());The mock is used to execute the method that we are testing, and we can delete the assertion because the mock will take care of checking it when it goes out of scope.
ctx.sut
.start_world_domination_stage1(&mut mock_henchman, &gdummy);
If we run the tests with cargo t --lib, they should all pass.
So far so good, but henchmen were used in a test with more logic involved. We want to verify that henchmen are instructed to both fight enemies and do hard things. Both methods must be invoked, but in a defined order.
Let's first try to state the two expectations and solve this with what we have learned so far.
fn world_domination_stage2_tells_henchman_to_do_hard_things_and_fight_with_enemies(
ctx: &mut Context,
) {
let mut mock_henchman = MockHenchman::new();
mock_henchman.expect_fight_enemies().once().return_const(());
mock_henchman
.expect_do_hard_things()
.once()
.return_const(());
ctx.sut.start_world_domination_stage2(mock_henchman);
}
We run the test and see that it passes. However, you will notice that I have stated the expectations in a different
order than the one that is used in the code. Changing the order the other way around doesn't change the result. In
many cases, that can be fine, but what happens if the order is relevant? mockall provides Sequence to help us
specify the order in which things happen. We create the Sequence and add each of the expectations to it in the
expected order.
Let's change the test to make the order relevant.
let mut sequence = Sequence::new();
mock_henchman
.expect_fight_enemies()
.once()
.in_sequence(&mut sequence)
.return_const(());
mock_henchman
.expect_do_hard_things()
.once()
.in_sequence(&mut sequence)
.return_const(());
Run the tests with cargo t --lib and experiment with the order in which the Henchman performs actions in the method
being tested. Our non-trivial logic can be successfully checked.
Before we move on, we can also delete the HenchmanDouble. One more left to go.
Implement custom behavior in the mock
Until now, our mock has replied with hard-coded answers. But what if we want to return data that depends on the input
to our member functions? mockall to the rescue again 🦸.
As in the previous cases, we can start by importing automock in cipher.rs.
#[cfg(test)]
use mockall::automock;
And we enable auto-mocking for the Cipher.
#[cfg_attr(test, automock)]
pub trait Cipher {
We go back to supervillain.rs and import MockCipher into the tests.
use crate::henchman::MockCipher;
In the test tell_plans_sends_ciphered_message(), we set the expectation for the cipher and use a closure to compute
the simplified logic for "ciphering".
let mut mock_cipher = MockCipher::new();
mock_cipher
.expect_transform()
.returning(|secret, _| String::from("+") + secret + "+");
ctx.sut
.tell_plans(test_common::MAIN_SECRET_MESSAGE, &mock_cipher);
The closure takes the same two arguments as the original transform() method and returns the same value –a String–.
It uses the secret string to apply a simple change that proves the cipher has been used, returning a modified version
of it.
Run the tests with cargo t --list and see them all pass. You have used mockall to simplify your tests. Congrats!
As a last step, delete the CipherDouble test double. We have eliminated more characters in this code, the
supervillains themselves. Do you feel the power of the dark side?
Final code
This time, we have introduced several changes to our code and removed many lines that are no longer needed. The code in
the supervillain.rs file is provided below, but please note that we also made some changes to sidekick.rs. You can
check the whole project, as always, in the corresponding commits of the repo with all the code in this series.
//! Module for supervillains and their related stuff
#![allow(unused)]
use std::time::Duration;
#[cfg(test)]
use mockall::automock;
#[cfg(test)]
use mockall_double::double;
use rand::Rng;
use thiserror::Error;
#[cfg_attr(test, double)]
use crate::sidekick::Sidekick;
use crate::{Cipher, Gadget, Henchman};
/// Type that represents supervillains.
#[derive(Default)]
pub struct Supervillain<'a> {
pub first_name: String,
pub last_name: String,
pub sidekick: Option<Sidekick<'a>>,
pub shared_key: String,
}
#[cfg_attr(test, automock)]
pub trait Megaweapon {
fn shoot(&self);
}
impl Supervillain<'_> {
/// Return the value of the full name as a single string.
///
/// Full name is produced concatenating first name, a single space, and the last name.
///
/// # Examples
/// ```
///# use evil::supervillain::Supervillain;
/// let lex = Supervillain {
/// first_name: "Lex".to_string(),
/// last_name: "Luthor".to_string(),
/// };
/// assert_eq!(lex.full_name(), "Lex Luthor");
/// ```
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
pub fn set_full_name(&mut self, name: &str) {
let components = name.split(" ").collect::<Vec<_>>();
println!("Received {} components.", components.len());
if components.len() != 2 {
panic!("Name must have first and last name");
}
self.first_name = components[0].to_string();
self.last_name = components[1].to_string();
}
pub fn attack(&self, weapon: &impl Megaweapon, intense: bool) {
weapon.shoot();
if intense {
let mut rng = rand::rng();
let times = rng.random_range(1..3);
for _ in 0..times {
weapon.shoot();
}
}
}
pub async fn come_up_with_plan(&self) -> String {
tokio::time::sleep(Duration::from_millis(100)).await;
String::from("Take over the world!")
}
pub fn conspire(&mut self) {
if let Some(ref sidekick) = self.sidekick {
if !sidekick.agree() {
self.sidekick = None;
}
}
}
pub fn start_world_domination_stage1<H: Henchman, G: Gadget>(
&self,
henchman: &mut H,
gadget: &G,
) {
if let Some(ref sidekick) = self.sidekick {
let targets = sidekick.get_weak_targets(gadget);
if !targets.is_empty() {
henchman.build_secret_hq(targets[0].clone());
}
}
}
pub fn start_world_domination_stage2<H: Henchman>(&self, henchman: H) {
henchman.fight_enemies();
henchman.do_hard_things();
}
pub fn tell_plans<C: Cipher>(&self, secret: &str, cipher: &C) {
if let Some(ref sidekick) = self.sidekick {
let ciphered_msg = cipher.transform(secret, &self.shared_key);
sidekick.tell(&ciphered_msg);
}
}
}
impl TryFrom<&str> for Supervillain<'_> {
type Error = EvilError;
fn try_from(name: &str) -> Result<Self, Self::Error> {
let components = name.split(" ").collect::<Vec<_>>();
if components.len() < 2 {
Err(EvilError::ParseError {
purpose: "full_name".to_string(),
reason: "Too few arguments".to_string(),
})
} else {
Ok(Supervillain {
first_name: components[0].to_string(),
last_name: components[1].to_string(),
..Default::default()
})
}
}
}
#[derive(Error, Debug)]
pub enum EvilError {
#[error("Parse error: purpose='{}', reason='{}'", .purpose, .reason)]
ParseError { purpose: String, reason: String },
}
#[cfg(test)]
mod tests {
use assertables::{assert_matches, assert_none, assert_some, assert_some_eq_x};
use mockall::{Sequence, predicate::eq};
use test_context::{AsyncTestContext, TestContext, test_context};
use crate::{cipher::MockCipher, gadget::MockGadget, henchman::MockHenchman, test_common};
use super::*;
#[test_context(Context)]
#[test]
fn full_name_is_first_name_space_last_name(ctx: &mut Context) {
let full_name = ctx.sut.full_name();
assert_eq!(
full_name,
test_common::PRIMARY_FULL_NAME,
"Unexpected full name"
);
}
#[test_context(Context)]
#[test]
fn set_full_name_sets_first_and_last_names(ctx: &mut Context) {
ctx.sut.set_full_name(test_common::SECONDARY_FULL_NAME);
// assert2::check!(ctx.sut.first_name == "A");
// assert2::assert!(ctx.sut.last_name == "B");
assert2::check!(ctx.sut.first_name == test_common::SECONDARY_FIRST_NAME);
assert2::assert!(ctx.sut.last_name == test_common::SECONDARY_LAST_NAME);
}
#[test_context(Context)]
#[test]
#[should_panic(expected = "Name must have first and last name")]
fn set_full_name_panics_with_empty_name(ctx: &mut Context) {
ctx.sut.set_full_name("");
}
#[test]
fn try_from_str_slice_produces_supervillain_full_with_first_and_last_name()
-> Result<(), EvilError> {
let sut = Supervillain::try_from(test_common::SECONDARY_FULL_NAME)?;
assert_eq!(sut.first_name, test_common::SECONDARY_FIRST_NAME);
assert_eq!(sut.last_name, test_common::SECONDARY_LAST_NAME);
Ok(())
}
#[test]
fn try_from_str_slice_produces_error_with_less_than_two_substrings() {
let result = Supervillain::try_from("");
let Err(error) = result else {
panic!("Unexpected value returned by try_from");
};
assert_matches!(error, EvilError::ParseError { purpose, reason } if purpose =="full_name" && reason == "Too few arguments");
}
#[test_context(Context)]
#[test]
fn non_intensive_attack_shoots_weapon_once(ctx: &mut Context) {
let mut weapon = MockMegaweapon::new();
weapon.expect_shoot().once().return_const(());
ctx.sut.attack(&weapon, false);
}
#[test_context(Context)]
#[test]
fn intensive_attack_shoots_weapon_twice_or_more(ctx: &mut Context) {
let mut weapon = MockMegaweapon::new();
weapon.expect_shoot().times(2..=3).return_const(());
ctx.sut.attack(&weapon, true);
}
#[test_context(Context)]
#[tokio::test]
async fn plan_is_sadly_expected(ctx: &mut Context<'_>) {
assert_eq!(ctx.sut.come_up_with_plan().await, "Take over the world!");
}
#[test_context(Context)]
#[test]
fn keep_sidekick_if_agrees_with_conspiracy(ctx: &mut Context) {
let mut mock_sidekick = Sidekick::new();
mock_sidekick.expect_agree().once().return_const(true);
ctx.sut.sidekick = Some(mock_sidekick);
ctx.sut.conspire();
assert_some!(&ctx.sut.sidekick, "Sidekick fired unexpectedly");
}
#[test_context(Context)]
#[test]
fn fire_sidekick_if_doesnt_agree_with_conspiracy(ctx: &mut Context) {
let mut mock_sidekick = Sidekick::new();
mock_sidekick.expect_agree().once().return_const(false);
ctx.sut.sidekick = Some(mock_sidekick);
ctx.sut.conspire();
assert_none!(&ctx.sut.sidekick, "Sidekick not fired unexpectedly");
}
#[test_context(Context)]
#[test]
fn conspiracy_without_sidekick_doesnt_fail(ctx: &mut Context) {
ctx.sut.conspire();
assert_none!(&ctx.sut.sidekick, "Unexpected sidekick");
}
#[test_context(Context)]
#[test]
fn world_domination_stage1_builds_hq_in_first_weak_target(ctx: &mut Context) {
let gdummy = MockGadget::new();
let mut mock_henchman = MockHenchman::new();
mock_henchman
.expect_build_secret_hq()
.with(eq(String::from(test_common::FIRST_TARGET)))
.return_const(());
let mut mock_sidekick = Sidekick::new();
mock_sidekick
.expect_get_weak_targets()
.once()
.returning(|_| test_common::TARGETS.map(String::from).to_vec());
ctx.sut.sidekick = Some(mock_sidekick);
ctx.sut
.start_world_domination_stage1(&mut mock_henchman, &gdummy);
}
#[test_context(Context)]
#[test]
fn world_domination_stage2_tells_henchman_to_do_hard_things_and_fight_with_enemies(
ctx: &mut Context,
) {
let mut mock_henchman = MockHenchman::new();
let mut sequence = Sequence::new();
mock_henchman
.expect_fight_enemies()
.once()
.in_sequence(&mut sequence)
.return_const(());
mock_henchman
.expect_do_hard_things()
.once()
.in_sequence(&mut sequence)
.return_const(());
ctx.sut.start_world_domination_stage2(mock_henchman);
}
#[test_context(Context)]
#[test]
fn tell_plans_sends_ciphered_message(ctx: &mut Context) {
let mut mock_sidekick = Sidekick::new();
mock_sidekick
.expect_tell()
.with(eq(String::from(test_common::MAIN_CIPHERED_MESSAGE)))
.once()
.return_const(());
ctx.sut.sidekick = Some(mock_sidekick);
let mut mock_cipher = MockCipher::new();
mock_cipher
.expect_transform()
.returning(|secret, _| String::from("+") + secret + "+");
ctx.sut
.tell_plans(test_common::MAIN_SECRET_MESSAGE, &mock_cipher);
}
struct Context<'a> {
sut: Supervillain<'a>,
}
impl<'a> AsyncTestContext for Context<'a> {
async fn setup() -> Context<'a> {
Context {
sut: Supervillain {
first_name: test_common::PRIMARY_FIRST_NAME.to_string(),
last_name: test_common::PRIMARY_LAST_NAME.to_string(),
..Default::default()
},
}
}
async fn teardown(self) {}
}
}Summary
In this article, I explain how to use a mocking library to create, control, and validate our test doubles. The
underlying knowledge of how they work stays the same, but we have used mockall to simplify writing them. The final
code will produce the same results with less effort and, hopefully, greater clarity.
One non-trivial observation is that the functionality provided by mockall to express expectations over test doubles
coexists with other assertions, either the basic ones provided by the standard library or the fancy ones provided by the
assertion crates. mockall expectations are used to express requirements on the test doubles, while assertions are
used for checking things about the type we are writing the tests for. You can also think about it from a different
perspective: mockall expectations are used for behavior tests, and assertions are used for state and return tests.
Stay curious. Hack your code. See you next time!
Footnotes
Don't worry, we aren't going backwards. This still quite automatic, but not as "automagic" as the automock attribute.