Running unit tests on substrate pallet | Polkadot Study (original) (raw)

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
cd Running-unit-test-on-substrate-pallet

Checkout to faulty 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 insrc/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 theframe_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
}

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 animpl 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:

Additionally, we looked at important substrate APIs including:

This article was focused on testing the functionalities of pallets, we didnot 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 formto help us measure our progress and improve Substrate in Bits content. It will only take 2 minutes of your time. Thank you!