Skip to main content

Running unit tests on substrate pallet

Testing the functionality of any software is an essential component of the software development lifecycle. Unit testing in substrate allows you to confirm that the methods exposed by a pallet are logically correct. Testing also enables you to ascertain that data and events related to a pallet are handled correctly when interacting with the pallet.

Substrate provides a comprehensive set of APIs that allow you to set up a test environment. This test environment can mock substrate runtime and simulate transaction execution for extrinsics and queries of your runtime.

In this guide, we will walk through a common problem related to mocking a runtime and testing a substrate pallet. We will also have an in-depth look at some crucial APIs that substrate exposes for testing and how to leverage them for complex testing scenarios.

Help us measure our progress and improve Substrate in Bits content by filling out our living feedback form. Thank you!

Reproducing errors

Environment and project setup

To follow along with this tutorial, ensure that you have the Rust toolchain installed.

git clone https://github.com/cenwadike/Running-unit-test-on-substrate-pallet
  • Navigate into the project’s directory.
cd Running-unit-test-on-substrate-pallet

Checkout to faulty test.

git checkout 1a7a90c
  • Run the command below to build the pallet.
cargo build --release
  • Run the command below to test the pallet.
cargo test 

While attempting to run the test, you’ll encounter an error like the one below:

error[E0046]: not all trait items implemented, missing: `RuntimeEvent`
--> src/mock.rs:52:1
|
52 | impl pallet_archiver::Config for Test {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `RuntimeEvent` in implementation
|
::: src/lib.rs:46:9
|
46 | type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
| ------------------------------------------------------------------------------------------- `RuntimeEvent` from trait

This compiler error tells us that RuntimeEvent is not implemented on the mock runtime. The error further points us to the Pallet Config trait in src/lib.rs where RuntimeEvent is defined.

Solving the error

We may recall that substrate exposes a rich set of APIs that allows us to mock a runtime without having to scaffold a full-blown runtime ourselves. However, to couple our pallet to this mock runtime, we must implement the Config trait of our pallet on the mock runtime.

A careful inspection of src/mock.rs reveals that a mock runtime was constructed using the FRAME construct_runtime macro taking Test enum as an argument for the mock runtime. This Test must contain trait definitions for each of the pallets that are used in the mock runtime.

The Test enum in src/mock.rs contains the trait definition for the frame_system pallet and our custom archiver_pallet like so:

// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system,
ArchiverPallet: pallet_archiver,
}
);

Because Test only needs to define the pallet, each pallet's configuration trait must be implemented (separately). As you may have observed in src/mock.rs, each pallet's Config trait was implemented for Test and all relevant Config types were defined for each pallet.

Our error resulted from the missing implementation of the RuntimeEvent trait type in the archiver_pallet Config implementation.

The solution to the compiler error is to implement RuntimeEvent trait type like so:

impl pallet_archiver::Config for Test {
type RuntimeEvent = RuntimeEvent; // <-- add this line
}
  • Re-run the test
cargo test

Going in-depth

In the previous section, we focused on how we can use substrate APIs to construct a mock runtime for tests. What we did not comment on, is how to mimic transactions and storage queries.

Substrate exposes an I/O interface that enables you to run several tests independently of each other through sp_io::TestExternalities.
sp_io::TestExternalities is an alias of sp_state_machine::TestExternalities.

sp_state_machine::TestExternalities can be viewed as a class that implements a wide array of methods that allows you to interact with a mock runtime.

sp_state_machine::TestExternalities is implemented as a struct with an impl block. Its impl contains several helpful methods that can be employed in different test scenarios. A particular commonly used method is execute_with which is implemented like so:

impl<H> TestExternalities<H>
where
H: Hasher + 'static,
H::Out: Ord + 'static + codec::Codec,
{
// ----- *snip* ------

pub fn execute_with<R>(&mut self, execute: impl FnOnce() -> R) -> R {
let mut ext = self.ext();
sp_externalities::set_and_run_with_externalities(&mut ext, execute)
}

// ----- *snip* ------
}

execute_with exposes the sp_state_machine::TestExternalities constructed from our Test enum to a test case and emulates the execution of a substrate extrinsic and runtime storage query.

You may observe in src/mock.rs that TestExternalities of mock runtime was constructed and exposed in our pallet crate like so:

pub fn new_test_env() -> sp_io::TestExternalities {
system::GenesisConfig::default()
.build_storage::<Test>()
.unwrap()
.into()
}

From this, we can construct a test for archiver_pallet like so:

#[test]
fn archive_book_works() {
new_test_env().execute_with(|| {
// ----- *snip* ------

assert_ok!(ArchiverPallet::archive_book(
RuntimeOrigin::signed(1),
title.clone(),
author.clone(),
url.clone(),
));

// ----- *snip* ------
});
}

We can observe that sp_state_machine::TestExternalities allows us to also query the storage of the mock runtime like so:

#[test]
fn archive_book_works() {
new_test_env().execute_with(|| {
// ----- *snip* ------

let url: Vec<u8> = "url".into();

// ----- *snip* ------

let stored_book_summary = ArchiverPallet::book_summary(hash).unwrap();
assert_eq!(stored_book_summary.url, url);
});
}

Coupling multiple pallets

It is important to know that every step of testing in substrate can be customized for your specific use case. This includes adding external pallets as dependencies on the mock runtime.

A look at the construct_runtime documentation gives a clue about how we can include external pallets into our mock runtime. We can do this like so:

// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system,
ArchiverPallet: pallet_archiver,
AnotherPallet: path::to::pallet_another, // <-- another pallet trait type
}
);
// Implement another pallet Config trait
impl pallet_another::Config for Test {
type ConfigType = ConcreteConfigType;
}

You should also ensure that all external pallets are added as dependencies in Cargo.toml file and imported into src/mock.rs.

You can also further specify what parts of a pallet you need in your mock runtime like so:

// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Event<T>, Config<T>},
ArchiverPallet: pallet_archiver,
AnotherPallet: path::to::pallet_another::{Pallet, Call},
}
);

Summary

In this guide, we explored a common problem when mocking a runtime for testing substrate pallets. We also developed an understanding of:

  • how to create a mock runtime for different test scenarios.
  • how to include custom and external pallets in a mock runtime.
  • how to mimic substrate extrinsic on a mock runtime.
  • how to query the storage of a mock runtime.

Additionally, we looked at important substrate APIs including:

  • frame_system construct_runtime macro for building a runtime from provided pallets.
  • sp_io::TestExternalities for interacting with a mock runtime through a test case.

This article was focused on testing the functionalities of pallets, we did not learn how to test a full node. In a future article, we will look at implementing test scenarios on full node, so be on the lookout for this.

To learn more about testing in substrate, check out these resources:

We’re inviting you to fill out our living feedback form to help us measure our progress and improve Substrate in Bits content. It will only take 2 minutes of your time. Thank you!

grillchat icon