Runtime and Tests
The last thing we will cover in this section is a small dive into constructing a blockchain runtime and writing tests.
Our pallet could be dependant on many "external" factors:
- The current block number.
- Hooks when new blocks are built.
- Other pallets, for example to manage the blockchain balance.
- Specific configuration of your blockchain.
- etc...
All of these things are managed outside of our pallet, thus to write tests for a pallet, we must create a "test blockchain" where we would include and use the pallet.
A whole tutorial could be written just about configuring a runtime and writing tests, but that would be too much for this tutorial. Instead, we will just go over the basics, and hopefully in the near future, we can make time to write a dedicated tutorial for this and add a link to that here.
The content on this page is not super relevant for the tutorial, so if you choose to skip this section, or come back to it later, you won't miss much. But definitely you will want to come back to it later, as you can't become a good Polkadot SDK developer without writing tests!
Constructing a Runtime
As we briefly covered earlier, the runtime is the state transition function for our blockchain.
In the context of writing unit tests for our pallet, we need not actually run a full, decentralized blockchain network, we just need to construct a runtime which places our custom pallet into our state transition function, and allows us to access it.
#[runtime]
Macro
The #[runtime]
macro does all the work to build that state transition function that we can run tests on top of. You will see that much like our pallet macros, the runtime macros have an entry point:
#![allow(unused)] fn main() { #[runtime] mod runtime { // -- snip -- } }
You can see inside this entrypoint, we have various sub-macros:
#[runtime::runtime]
#[runtime::derive]
#[runtime::pallet_index(n)]
While the runtime
module does not look super big, you should know it generates a LOT of code. Much of it is totally hidden from you, since it is all dynamically generated boilerplate code to interface our runtime to the rest of our blockchain.
Let's look a little closer into these different sub-macros.
#[runtime::runtime]
Our whole blockchain runtime is represented by a single struct with the #[runtime:runtime]
attribute:
#![allow(unused)] fn main() { #[runtime::runtime] pub struct Runtime; }
You can name this struct whatever you want. As you see in our tests, we name it TestRuntime
for additional clarity. When you build a full Polkadot SDK project, you will probably have multiple runtimes, some for unit tests, some for test networks, and some for production. Because the Polkadot SDK is designed to be modular and configurable, it is super easy to do this, and construct many versions of your blockchain runtime
You can think of this runtime as just a placeholder for all of our runtime configuration and traits. The TestRuntime
does not actually hold any data. It is a
More specifically, if you remember the Config
trait that we must implement, TestRuntime
will be the struct that implements all those traits and satisfies Config
. We will see this below.
#[runtime::derive]
The runtime macros generate a lot of objects which give access to our state transition function and the pallets integrated in them.
You have already learned that pallets have:
- Callable Functions
- Events
- Errors
- etc...
The runtime macros generate "aggregated" runtime enums which represents all of those things across all pallets.
For example, imagine our blockchain has two pallets, each with one event. That would mean in our codebase, we would have two enums which look something like:
#![allow(unused)] fn main() { // Found in our Pallet 1 crate. enum Pallet1Event { Hello, } // Found in our Pallet 2 crate. enum Pallet2Event { World, } }
Our #[runtime::derive(RuntimeEvent)]
would aggregate these together, and allow you to access all possible events from a single object:
#![allow(unused)] fn main() { // Constructed by our enum RuntimeEvent { Pallet1(Pallet1Event), Pallet2(Pallet2Event) } }
NOTE: If you want to dive deeper into this, be sure to check out the
rust-state-machine
tutorial.
So at a high level, the runtime derive macros generate all of these aggregated types, which become available and can be used in our runtime and blockchain.
#[runtime::pallet_index(n)]
As we discussed earlier, FRAME's opinion on how to build a blockchain runtime is by allowing users to split up their state transition function into individual modules which we call pallets.
With the pallet index macro, you can literally see how we can compose a new runtime using a collection of pallets.
Our test runtime only needs three pallets to allow us to write good unit tests:
frame_system
: This is required for any FRAME based runtime, so always included.pallet_balances
: This is a pallet which manages a blockchain's native currency, which will be used by our custom pallet.pallet_kitties
: This is the custom pallet we are building in this tutorial, and that we will test the functionality of.
You can see adding a pallet to your runtime is pretty simple:
#![allow(unused)] fn main() { #[runtime::pallet_index(2)] pub type PalletKitties = pallet_kitties::Pallet<TestRuntime>; }
Each pallet needs a unique index, and right now we only support 256 pallets maximum.
You can see that we extract the Pallet
struct from each of the pallet crates. Since the Pallet
struct is generic over T: Config
, and because the TestRuntime
struct will implement all the required traits, we can use it.
We can assign this to a type with any name we choose. To make things simple and explicit, we chose the name PalletKitties
, which we can use to reference this specific pallet in all of our tests.
Configuring your Runtime
Right below the mod runtime
block, you will see us implement all the Config
traits for our different pallets on the TestRuntime
object.
#![allow(unused)] fn main() { impl pallet_kitties::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; } }
This trait is named "Config", and you can really think about this as the configuration for each pallet in your runtime.
You can see pallet_kitties
only exposes one configuration, which is basically asking us to pass back to it the RuntimeEvent
generated by the #[runtime::derive]
macro. If we didn't configure this right, our pallet would not be able to emit events (or even compile).
The frame_system
and pallet_balances
pallets also have a ton of configurations, but most of those are hidden and automatically configured thanks to the config_preludes::TestDefaultConfig
:
#![allow(unused)] fn main() { #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for TestRuntime { type Block = Block; type AccountData = pallet_balances::AccountData<Balance>; } }
Configuring pallets is very specific to each pallet you add to your blockchain, and requires you to read the documentation of that pallet. These default configurations are suitable for more unit tests, but depending on your needs, you might want to change some of the configuration choices.
For the purposes of this tutorial, these TestDefaultConfig
options are exactly what we need.
Writing Tests
Now that we have constructed a test runtime with all the pallets we want to include, we can actually start writing unit tests.
Test Externalities
Unit tests for the Polkadot SDK are just normal rust tests, but calling into our test runtime.
However, in a regular blockchain, you would have a database, and your pallet and pallet storage would call into this database and actually store the changes caused by your state transition function.
In our test environment, we must introduce a storage abstraction that will maintain state during a test and reset at the end.
For this, we create a new test externalities:
#![allow(unused)] fn main() { pub fn new_test_ext() -> sp_io::TestExternalities { frame_system::GenesisConfig::<TestRuntime>::default() .build_storage() .unwrap() .into() } }
To use this text externalities, you need to execute your tests within a closure:
#![allow(unused)] fn main() { #[test] fn my_pallet_test() { new_test_ext().execute_with(|| { // Your pallet test here. }); } }
If you write a pallet test which uses some storage, and forget to wrap it inside the test externalities, you will get an error:
#![allow(unused)] fn main() { #[test] fn forgot_new_test_ext() { System::set_block_number(1); } }
---- tests::forgot_new_test_ext stdout ----
thread 'tests::forgot_new_test_ext' panicked at /Users/shawntabrizi/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sp-io-38.0.0/src/lib.rs:205:5:
`set_version_1` called outside of an Externalities-provided environment.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This error just tells you to add the new_test_ext
wrapper.
Calling Pallets
In order to setup and execute our various tests, we need to call into the pallets in our runtime.
Looking back at our runtime definition, we created a bunch of types representing our pallets, and we can use those to access our pallet's functions.
You already saw an example of us accessing the frame_system::Pallet
with System::set_block_number(1)
.
Here, we are calling a function implemented on the Pallet
in the crate frame_system
:
#![allow(unused)] fn main() { // In the `frame_system` crate... impl<T: Config> Pallet<T> { fn set_block_number(n: T::BlockNumber) { // -- snip -- } } }
If, for example, you wanted to call the mint
function in the pallet you are working on and ensure the mint succeeded, you would simply write:
#![allow(unused)] fn main() { assert_oK!(PalletKitties::mint(some_account)); }
This is not any kind of Polkadot SDK specific magic, this is just regular Rust.
Checking Events
One of the ways you can check that your test goes right is by looking at the events emitted at the end of your call.
For this, you can use System::assert_last_event(...)
, which checks in storage what the last event emitted by any pallet was.
You can see an example of this added to our tests.rs
file in this step.
One really important thing to remember is that you need to set the block number to a value greater than zero for events to work! This is because on the genesis block, we don't want to emit events, because there will be so many of them, it would bloat and lag our blockchain on that zeroth block.
If you write a test, and you expect some event, but don't see it, just double check that you have set the block number.
Your Turn!
Don't forget to update your tests.rs
file to include the test provided in this step.
It shows how you can:
- Set the blocknumber of your blockchain inside your tests.
- Call an extrinsic in your pallet from an
AccountId
of your choice. - Check the extrinsic call completed
Ok(())
. - Get the last event deposited into
System
. - Check that last event matches the event you would expect from your pallet.
There is so much more that can be taught about tests, but it really makes sense to cover these things AFTER you have learned all the basics about building a pallet. There is a lot of content here already, and truthfully, it is not super important for completing this tutorial. However, writing tests is a critically important for actually creating production ready systems.
From this point forward, every step where you write some code will include new tests or modify existing tests.
Make sure to keep updating your tests.rs
file throughout the tutorial.
#![allow(unused)] fn main() { // Tests for the Kitties Pallet. // // Normally this file would be split into two parts: `mock.rs` and `tests.rs`. // The `mock.rs` file would contain all the setup code for our `TestRuntime`. // Then `tests.rs` would only have the tests for our pallet. // However, to minimize the project, these have been combined into this single file. // // Learn more about creating tests for Pallets: // https://paritytech.github.io/polkadot-sdk/master/polkadot_sdk_docs/guides/your_first_pallet/index.html // This flag tells rust to only run this file when running `cargo test`. #![cfg(test)] use crate as pallet_kitties; use crate::*; use frame::deps::frame_support::runtime; use frame::deps::sp_io; use frame::runtime::prelude::*; use frame::testing_prelude::*; use frame::traits::fungible::*; type Balance = u64; type Block = frame_system::mocking::MockBlock<TestRuntime>; // In our "test runtime", we represent a user `AccountId` with a `u64`. // This is just a simplification so that we don't need to generate a bunch of proper cryptographic // public keys when writing tests. It is just easier to say "user 1 transfers to user 2". // We create the constants `ALICE` and `BOB` to make it clear when we are representing users below. const ALICE: u64 = 1; const BOB: u64 = 2; /* 🚧 TODO 🚧: Learn about constructing a runtime. */ #[runtime] mod runtime { #[runtime::derive( RuntimeCall, RuntimeEvent, RuntimeError, RuntimeOrigin, RuntimeTask, RuntimeHoldReason, RuntimeFreezeReason )] #[runtime::runtime] /// The "test runtime" that represents the state transition function for our blockchain. /// /// The runtime is composed of individual modules called "pallets", which you find see below. /// Each pallet has its own logic and storage, all of which can be combined together. pub struct TestRuntime; /// System: Mandatory system pallet that should always be included in a FRAME runtime. #[runtime::pallet_index(0)] pub type System = frame_system::Pallet<TestRuntime>; /// PalletBalances: Manages your blockchain's native currency. (i.e. DOT on Polkadot) #[runtime::pallet_index(1)] pub type PalletBalances = pallet_balances::Pallet<TestRuntime>; /// PalletKitties: The pallet you are building in this tutorial! #[runtime::pallet_index(2)] pub type PalletKitties = pallet_kitties::Pallet<TestRuntime>; } /* 🚧 TODO 🚧: Learn about configuring a pallet. */ // Normally `System` would have many more configurations, but you can see that we use some macro // magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for TestRuntime { type Block = Block; type AccountData = pallet_balances::AccountData<Balance>; } // Normally `pallet_balances` would have many more configurations, but you can see that we use some // macro magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for TestRuntime { type AccountStore = System; type Balance = Balance; } // This is the configuration of our Pallet! If you make changes to the pallet's `trait Config`, you // will also need to update this configuration to represent that. impl pallet_kitties::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; } /* 🚧 TODO 🚧: Learn about test externalities. */ // We need to run most of our tests using this function: `new_test_ext().execute_with(|| { ... });` // It simulates the blockchain database backend for our tests. // If you forget to include this and try to access your Pallet storage, you will get an error like: // "`get_version_1` called outside of an Externalities-provided environment." pub fn new_test_ext() -> sp_io::TestExternalities { frame_system::GenesisConfig::<TestRuntime>::default() .build_storage() .unwrap() .into() } #[test] fn starting_template_is_sane() { new_test_ext().execute_with(|| { let event = Event::<TestRuntime>::Created { owner: ALICE }; let _runtime_event: RuntimeEvent = event.into(); let _call = Call::<TestRuntime>::create_kitty {}; let result = PalletKitties::create_kitty(RuntimeOrigin::signed(BOB)); assert_ok!(result); }); } #[test] fn system_and_balances_work() { // This test will just sanity check that we can access `System` and `PalletBalances`. new_test_ext().execute_with(|| { // We often need to add some balance to a user to test features which needs tokens. assert_ok!(PalletBalances::mint_into(&ALICE, 100)); assert_ok!(PalletBalances::mint_into(&BOB, 100)); }); } #[test] fn create_kitty_checks_signed() { new_test_ext().execute_with(|| { // The `create_kitty` extrinsic should work when being called by a user. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // The `create_kitty` extrinsic should fail when being called by an unsigned message. assert_noop!(PalletKitties::create_kitty(RuntimeOrigin::none()), DispatchError::BadOrigin); }) } /* 🚧 TODO 🚧: Learn about writing tests. */ #[test] fn create_kitty_emits_event() { new_test_ext().execute_with(|| { // We need to set block number to 1 to view events. System::set_block_number(1); // Execute our call, and ensure it is successful. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Assert the last event by our blockchain is the `Created` event with the correct owner. System::assert_last_event(Event::<TestRuntime>::Created { owner: 1 }.into()); }) } }
diff --git a/src/tests.rs b/src/tests.rs
index d87be46f..434ce723 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -29,6 +29,7 @@ type Block = frame_system::mocking::MockBlock<TestRuntime>;
const ALICE: u64 = 1;
const BOB: u64 = 2;
+/* 🚧 TODO 🚧: Learn about constructing a runtime. */
#[runtime]
mod runtime {
#[runtime::derive(
@@ -60,6 +61,7 @@ mod runtime {
pub type PalletKitties = pallet_kitties::Pallet<TestRuntime>;
}
+/* 🚧 TODO 🚧: Learn about configuring a pallet. */
// Normally `System` would have many more configurations, but you can see that we use some macro
// magic to automatically configure most of the pallet for a "default test configuration".
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
@@ -82,6 +84,7 @@ impl pallet_kitties::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
}
+/* 🚧 TODO 🚧: Learn about test externalities. */
// We need to run most of our tests using this function: `new_test_ext().execute_with(|| { ... });`
// It simulates the blockchain database backend for our tests.
// If you forget to include this and try to access your Pallet storage, you will get an error like:
@@ -123,3 +126,16 @@ fn create_kitty_checks_signed() {
assert_noop!(PalletKitties::create_kitty(RuntimeOrigin::none()), DispatchError::BadOrigin);
})
}
+
+/* 🚧 TODO 🚧: Learn about writing tests. */
+#[test]
+fn create_kitty_emits_event() {
+ new_test_ext().execute_with(|| {
+ // We need to set block number to 1 to view events.
+ System::set_block_number(1);
+ // Execute our call, and ensure it is successful.
+ assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE)));
+ // Assert the last event by our blockchain is the `Created` event with the correct owner.
+ System::assert_last_event(Event::<TestRuntime>::Created { owner: 1 }.into());
+ })
+}