I want to present a crate we’ve developed as part of our work in the centrifuge parachain that can be useful for other Substrate projects. We try to give a solution to clear all boilerplate, dependencies, and knowledge needed to set up pallet tests.
Problem
As part of our development, we have needed to create pallets that depend on many other pallets. We always try to follow a loose coupling pattern, but all benefits of this are lost in the testing part. Once you create the mock.rs
file, you need to configure your pallet with other pallets, which need to be configured with their dependencies, and so on.
We could say that we are tightly coupled in testing, and most of the effort to make trait-based abstractions are lost. This tight coupling comes with some downsides to the testing part:
- You need to learn how to configure other pallets used as dependencies.
- You need to know how those pallets work because they directly affect the behavior of the pallet you are testing.
- The way they work can give you non-complete 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 them.
Solution
To solve this, we bring a utility to create mock pallets. We define a mock pallet as a pallet that implements any number of traits whose behaviors can be specified in each test case using rust closures. There are other crates as mockall
that mock traits. Instead, this crate gives you an entire functional pallet ready to use in any runtime, implementing any combination of traits.
For example, if we want to build a mock pallet that implements a trait that has a method fn foo(a: u32)
, this utility implements foo()
and builds a new method called mock_foo()
that can be used from the test cases as follows:
MyMockPallet::mock_foo(|a| {
// Mocking your behavior here
});
Each time the pallet calls internally to foo()
it will execute the closure we have defined above.
This solves the issues mentioned above, getting loose coupled tests:
- You no longer need to configure dependencies for testing your pallet.
- The behavior is defined for each of your use cases.
- You can call
mock_foo()
any number of times, returning all possible values to get complete tests. - You no longer need to fix your tests if some dependency changes because you have no dependencies!
Test it!
We have used this way of doing testing in our project with successful results, and we want to share the utility to help the community and get feedback to improve it.
Currently, it supports traits with complex generic methods and lifetimes, and we want to focus on improving the mock pallet creation part using procedural macros and removing the automatic boilerplate, which is now needed.
- You can check the docs, to get a deeper understanding of how it works and how to use it.
- You can use it in your project as follows (although we will possibly create a new repo to ease the maintainability):
mock-builder = { git = "https://github.com/centrifuge/centrifuge-chain", branch = "main" }
Happy to hear your thoughts on this!