Skip to content

Commit

Permalink
Add Section on Writing Tests (#196)
Browse files Browse the repository at this point in the history
* make space for new step

* update previous step

* init

* use TestRuntime

* updates

* Update README.md

* Delete out

* finish step
  • Loading branch information
shawntabrizi authored Dec 18, 2024
1 parent de4d70e commit 18e4324
Show file tree
Hide file tree
Showing 223 changed files with 6,667 additions and 2,668 deletions.
8 changes: 3 additions & 5 deletions steps/1/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ mod runtime {

/// System: Mandatory system pallet that should always be included in a FRAME runtime.
#[runtime::pallet_index(0)]
pub type System = frame_system::Pallet<Runtime>;
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<Runtime>;
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<Runtime>;
pub type PalletKitties = pallet_kitties::Pallet<TestRuntime>;
}

// Normally `System` would have many more configurations, but you can see that we use some macro
Expand Down Expand Up @@ -108,8 +108,6 @@ fn starting_template_is_sane() {
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 set `System` to block 1 so that we can see events.
System::set_block_number(1);
// 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));
Expand Down
265 changes: 262 additions & 3 deletions steps/10/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,264 @@
# Storage Basics
# Runtime and Tests

Now that we have covered the basics of Pallets and gone through all of the template code, we can start writing some code ourselves.
The last thing we will cover in this section is a small dive into constructing a blockchain runtime and writing tests.

In this section you will learn the basics of creating and using storage in your Pallet, including creating and using storage values and storage maps.
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:

```rust
#[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:

```rust
#[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:

```rust
// 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:

```rust
// 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`](https://github.com/shawntabrizi/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:

1. `frame_system`: This is required for any FRAME based runtime, so always included.
2. `pallet_balances`: This is a pallet which manages a blockchain's native currency, which will be used by our custom pallet.
3. `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:

```rust
#[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.

```rust
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`:

```rust
#[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:

```rust
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:

```rust
#[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:

```rust
#[test]
fn forgot_new_test_ext() {
System::set_block_number(1);
}
```

```text
---- 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`:

```rust
// 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:

```rust
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.
2 changes: 1 addition & 1 deletion steps/10/gitorial_metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"_Note": "This file will not be included in your final gitorial.",
"commitMessage": "section: storage basics"
"commitMessage": "action: learn about events"
}
1 change: 1 addition & 0 deletions steps/10/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}

/* 🚧 TODO 🚧: Learn about Pallet Events. */
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Expand Down
12 changes: 7 additions & 5 deletions steps/10/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -49,17 +50,18 @@ mod runtime {

/// System: Mandatory system pallet that should always be included in a FRAME runtime.
#[runtime::pallet_index(0)]
pub type System = frame_system::Pallet<Runtime>;
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<Runtime>;
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<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)]
Expand All @@ -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:
Expand All @@ -108,8 +111,6 @@ fn starting_template_is_sane() {
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 set `System` to block 1 so that we can see events.
System::set_block_number(1);
// 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));
Expand All @@ -126,6 +127,7 @@ fn create_kitty_checks_signed() {
})
}

/* 🚧 TODO 🚧: Learn about writing tests. */
#[test]
fn create_kitty_emits_event() {
new_test_ext().execute_with(|| {
Expand Down
Loading

0 comments on commit 18e4324

Please sign in to comment.