Writing unit tests for pallets
With our dispatchable function written, it's time to ensure we understand how to unit test it. Substrate and FRAME provide tools for unit tests without spinning up a local chain, including a mock environment and traditionally written unit tests.
Overview of mock.rs
The purpose of mock.rs
is to mock a runtime configuration. Upon observing it, you can notice that
most types are regular, primitive types that are in place of the more abstract types inside of
runtime/lib.rs
:
impl frame_system::Config for Test {
...
type Index = u64;
type BlockNumber = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
...
}
For example, AccountId
is u64
, meaning a valid account address can be an unsigned integer, i.e.,
let bob_account: u64 = 1;
The simplification of types is for simplifying the testing process.
Test Externalities
To configure and produce an environment, notice the function at the bottom of mock.rs
:
// Build genesis storage according to the mock runtime.
pub fn new_test_ext() -> sp_io::TestExternalities {
frame_system::GenesisConfig::default().build_storage::<Test>().unwrap().into()
}
This function defines a 'testbed' for your tests to simulate aspects like the storage layer.
Writing a unit test for register
Navigate to pallets/connect/src/tests.rs
Unit tests are placed within tests.rs
. They utilize the mock environment and configuration to test
various scenarios with the pallet. These are constructed like traditional Rust unit tests, with the
caveat that these tests must be run within the TestExternalities
instance defined in mock.rs
.
Using unit tests, we can test extrinsics, storage, errors, and events using our mock environment.
Unit Test Cheatsheet
For testing, there are numerous macros and APIs one could call:
assert!
,assert_ok!
, andassert_eq!
work as per normal unit testing in Rust.- To check if an event has been emitted, use
System::assert_last_event()
with the Event as the parameter. You may need to use.into()
. - To check if an error has been emitted, use
assert_noop!(call, Error::<Test>::SomeError);
. Notice the::<Test>::
turbofish syntax used to call the error with the Test config as the generic parameter. - If events aren't seemingly emitted within tests, be sure to set the testing environment to a
height of at least one:
System::set_block_number(1);
Low balance test example
While all the tests are in tests.rs
, here is an example of a unit test. FRAME provides numerous
helper APIs, such as System
, to perform actions like setting the block height for the test
environment. See it in action below:
#[test]
fn balance_too_low() {
new_test_ext().execute_with(|| {
let name = b"polkadot".to_vec();
let bio = b"A heterogeneous, sharded network.".to_vec();
// Go past genesis block so events get deposited
System::set_block_number(1);
// Set the balance to 9 DOT - too low
assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 1, 9));
// Dispatch a signed extrinsic.
assert_noop!(
Connect::register(RuntimeOrigin::signed(1), name.clone(), bio),
Error::<Test>::LowBalance
);
});
}