Crate mock_builder
source ·Expand description
mock-builder
allows you to create mock pallets.
A mock pallet is a regular pallet that implements some traits whose
behavior can be implemented on the fly by closures. They are perfect for
testing because they allow you to customize each test case, getting
organized and accurate tests for your pallet. Mock pallet is not just a
trait mocked, it’s a whole pallet that can implement one or more traits and
can be added to runtimes.
§Motivation
Pallets have dependencies. Programming in a
loosely coupled
way is great for getting rid of those dependencies for the implementation.
Nevertheless, those dependencies still exist in testing because when the
mock.rs
file is defined, you’re forced to give some implementations for
the associated types of your pallet Config
.
Then, you are mostly forced to use other pallet configurations getting a tight coupling with them. It has some downsides:
- You need to learn how to configure other pallets.
- You need to know how those pallets work, because they affect directly the behavior of the pallet you’re testing.
- The way they work can give you non-completed tests. It means that some paths of your pallet can not be tested because some dependency works in a specific way.
- You need a lot of effort maintaining your tests because each time one dependency changes, it can easily break your tests.
This doesn’t scale well. Frequently some pallet dependencies need in turn to configure their own dependent pallets, making this problem even worse.
This is why mocking is so important. It lets you get rid of all these dependencies and related issues, obtaining loose coupling tests.
There are other crates focusing on this problem,
such as mockall
,
but they mock traits. Instead, this crate gives you an entire pallet
ready to use in any runtime, implementing the number of traits you specify.
§Mock pallet usage
Suppose that in our pallet, which we’ll call it my_pallet
, we have an
associated type in our Config
, which implements traits TraitA
and
TraitB
. Those traits are defined as follows:
trait TraitA {
type AssocA;
fn foo() -> Self::AssocA;
}
trait TraitB {
type AssocB;
fn bar(a: u64, b: Self::AssocB) -> u32;
}
We have a really huge pallet that implements a specific behavior for those
traits, but we want to get rid of such dependency so we generate a mock
pallet, we’ll call it pallet_mock_dep
.
We can add this mock pallet to the runtime as usual:
frame_support::construct_runtime!(
pub struct Runtime {
System: frame_system,
MockDep: pallet_mock_dep,
MyPallet: my_pallet,
}
);
And we configure it as a regular pallet:
impl pallet_mock_dep::Config for Runtime {
type AssocA = bool;
type AssocB = u8;
}
Later in our use case, we can give a behavior for both foo()
and bar()
methods in their analogous methods mock_foo()
and mock_bar()
which
accept a closure.
#[test]
fn correct() {
new_test_ext().execute_with(|| {
MockDep::mock_foo(|| true);
MockDep::mock_bar(|a, b| {
assert_eq!(a, 42);
assert_eq!(b, false);
23
});
// This method will call foo() and bar() under the hood, running the
// closures we just have defined.
MyPallet::my_call();
});
}
Take a look to the pallet tests to have a user view of how to use a mock pallet. It supports any kind of trait, with reference parameters and generics at trait level and method level.
§Mock pallet creation
NOTE: There is a working progress on this part to generate mock pallets automatically using procedural macros. Once done, all this part can be auto-generated.
This crate exports two macros register_call!()
and execute_call!()
that allow you to build a mock pallet.
-
register_call!()
registers a closure where you can define the mock behavior for that method. The method which registers the closure must have the name of the trait method you want to mock prefixed withmock_
. -
execute_call!()
is placed in the trait method implementation and will call the closure previously registered byregister_call!()
The only condition to use these macros is to have the following storage in the pallet (it’s safe to just copy and paste this snippet in your pallet):
#[pallet::storage]
type CallIds<T: Config> = StorageMap<_, _, String, mock_builder::CallId>;
Following the above example, generating a mock pallet for both TraitA
and TraitB
is done as follows:
#[frame_support::pallet(dev_mode)]
pub mod pallet {
use frame_support::pallet_prelude::*;
use mock_builder::{execute_call, register_call};
#[pallet::config]
pub trait Config: frame_system::Config {
type AssocA;
type AssocB;
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::storage]
type CallIds<T: Config> = StorageMap<_, _, String, mock_builder::CallId>;
impl<T: Config> Pallet<T> {
fn mock_foo(f: impl Fn() -> T::AssocA + 'static) {
register_call!(move |()| f())
}
fn mock_bar(f: impl Fn(u64, T::AssocB) -> u32 + 'static) {
register_call!(move |(a, b)| f(a, b))
}
}
impl<T: Config> TraitA for Pallet<T> {
type AssocA = T::AssocA;
fn foo() -> Self::AssocA {
execute_call!(())
}
}
impl<T: Config> TraitB for Pallet<T> {
type AssocB = T::AssocB;
fn bar(a: u64, b: Self::AssocB) -> u32 {
execute_call!((a, b))
}
}
}
If types for the closure of mock_*
method and trait method don’t match,
you will obtain a runtime error in your tests.
§Mock Patterns
§Storage pattern
In some cases it’s pretty common making a mock that returns a value that was set previously by another mock. For this case you can define your “getter” mock inside the definition of the “setter” mock, as follows:
MyMock::mock_set(|value| MyMock::mock_get(move || value));
Any call to get()
will return the last value given to set()
.
§Check internal calls are ordered
If you want to test some mocks method are calle in some order, you can define them nested, in the expected order they must be called
MyMock::mock_first(|| {
MyMock::mock_second(|| {
MyMock::mock_third(|| {
//...
})
})
});
// The next method only will be succesful
// if it makes the internal calls in order
MyPallet::calls_first_second_third();
Re-exports§
pub use storage::CallId;
Modules§
- Provide functions for handle fuction locations
- Provide functions for register/execute calls This module is in change of storing closures with the type
Fn(I) -> O
in a static lifetime storage, supporting mixing differentsI
andO
types. Because we need to merge different closures with different types in the same storage, we use anu128
as closure identification (composed by the closure function pointer (u64
) and the pointer to the closure metadata (u64
).
Macros§
- Execute a function from the function storage. Same as
execute()
but it uses as locator who calls this macro. - Execute a function from the function storage for a pallet with instances. Same as
execute()
but it uses as locator who calls this macro. - Register a mock function into the mock function storage. Same as
register()
but it uses as locator who calls this macro. - Register a mock function into the mock function storage for a pallet with instances. Same as
register()
but it uses as locator who calls this macro.
Constants§
- Prefix that the register functions should have.
Functions§
- Execute a function from the function storage. This function should be called with a locator used as a function identification.
- Register a mock function into the mock function storage. This function should be called with a locator used as a function identification.