Testing

Runtime tests allow you to verify the logic in your runtime module by mocking a Substrate runtime environment.

Unit testing

Substrate uses the existing unit testing framework provided by Rust. To run tests, the command is

cargo test <optional: test_name>

Mock runtime environment

To test a Substrate runtime, construct a mock runtime environment. The configuration type Test is defined as a Rust enum with implementations for each of the pallet configuration trait that are used in the mock runtime.

frame_support::construct_runtime!(
    pub enum Test where
        Block = Block,
        NodeBlock = Block,
        UncheckedExtrinsic = UncheckedExtrinsic,
    {
        System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
        TemplateModule: pallet_template::{Pallet, Call, Storage, Event<T>},
    }
);

impl frame_system::Config for Test {
    // -- snip --
    type AccountId = u64;
}

If Test implements pallet_balances::Config, the assignment might use u64 for the Balance type.

impl pallet_balances::Config for Test {
    // -- snip --
    type Balance = u64;
}

By assigning pallet_balances::Balance and frame_system::AccountId to u64, mock runtimes ease the mental overhead of comprehensive, conscientious testers. Reasoning about accounts and balances only requires tracking a (AccountId: u64, Balance: u64) mapping.

Mock runtime storage

The sp-io crate exposes a TestExternalities implementation frequently used for mocking storage in tests. It is the type alias for an in-memory, hashmap-based externalities implementation in substrate_state_machine referred to as TestExternalities.

This example demonstrates defining a struct called ExtBuilder to build an instance of TestExternalities, and setting the block number to 1.

pub struct ExtBuilder;

impl ExtBuilder {
    pub fn build(self) -> sp_io::TestExternalities {
        let mut t = system::GenesisConfig::default().build_storage::<TestRuntime>().unwrap();
        let mut ext = sp_io::TestExternalities::new(t);
        ext.execute_with(|| System::set_block_number(1));
        ext
    }
}

To create the test environment in unit tests, the build method is called to generate a TestExternalities using the default genesis configuration.

#[test]
fn fake_test_example() {
    ExtBuilder::default().build_and_execute(|| {
        // ...test logics...
    });
}

Custom implementations of Externalities allow developers to construct runtime environments that provide access to features of the outer node. Another example of this can be found in offchain, which maintains its own Externalities implementation.

Genesis config

The previously shown ExtBuilder::build() method used the default genesis configuration for building the mock runtime environment. In many cases, it is convenient to set storage before testing.

An example might involve pre-seeding account balances before testing.

In the implementation of frame_system::Config, AccountId is set to u64 just like Balance shown above. Place (u64, u64) pairs in the balances vec to seed (AccountId, Balance) pairs as the account balances.

impl ExtBuilder {
    pub fn build(self) -> sp_io::TestExternalities {
        let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
        pallet_balances::GenesisConfig::<Test> {
            balances: vec![
                (1, 10),
                (2, 20),
                (3, 30),
                (4, 40),
                (5, 50),
                (6, 60)
            ],
        }
            .assimilate_storage(&mut t)
            .unwrap();

        let mut ext = sp_io::TestExternalities::new(t);
        ext.execute_with(|| System::set_block_number(1));
        ext
    }
}

Account 1 has balance 10, account 2 has balance 20, and so on.

The exact struct of how to define the genesis config of a certain pallet depends on the pallet GenesisConfig struct definition. For Balances Pallet, as shown in the rustdocs, it is defined as:

pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
    pub balances: Vec<(T::AccountId, T::Balance)>,
}

Block production

It will be useful to simulate block production to verify that expected behavior holds across block production.

A simple way of doing this is by incrementing the System module's block number between on_initialize and on_finalize calls from all modules with System::block_number() as the sole input. While it is important for runtime code to cache calls to storage or the system module, the test environment scaffolding should prioritize readability to facilitate future maintenance.

fn run_to_block(n: u64) {
    while System::block_number() < n {
        if System::block_number() > 1 {
            ExamplePallet::on_finalize(System::block_number());
            System::on_finalize(System::block_number());
        }
        System::set_block_number(System::block_number() + 1);
        System::on_initialize(System::block_number());
        ExamplePallet::on_initialize(System::block_number());
    }
}

on_finalize and on_initialize are only called from ExamplePallet if the pallet trait implements the frame_support::traits::{OnInitialize, OnFinalize} traits to execute the logic encoded in the runtime methods before and after each block respectively.

Then call this function in the following fashion.

#[test]
fn my_runtime_test() {
    with_externalities(&mut new_test_ext(), || {
        assert_ok!(ExamplePallet::start_auction());
        run_to_block(10);
        assert_ok!(ExamplePallet::end_auction());
    });
}
Last edit: on

Was This Page Helpful?
Help us improve