Pallet Unit Testing¶
Introduction¶
You have learned how to create a new pallet in the Build a Custom Pallet tutorial; now you will see how to test the pallet to ensure that it works as expected. As stated in the Pallet Testing article, unit testing is crucial for ensuring the reliability and correctness of pallets in Polkadot SDK-based blockchains. Comprehensive testing helps validate pallet functionality, prevent potential bugs, and maintain the integrity of your blockchain logic.
This tutorial will guide you through creating a unit testing suite for a custom pallet created in the Build a Custom Pallet tutorial, covering essential testing aspects and steps.
Prerequisites¶
To set up your testing environment for Polkadot SDK pallets, you'll need:
- Polkadot SDK dependencies installed
- Basic understanding of Substrate/Polkadot SDK concepts
- A custom pallet implementation, check the Build a Custom Pallet tutorial
- Familiarity with Rust testing frameworks
Set Up the Testing Environment¶
To effectively create the test environment for your pallet, you'll need to follow these steps:
-
Move to the project directory
-
Add the required dependencies to your test configuration in the
Cargo.toml
file of the pallet: -
Create a
mock.rs
and atests.rs
files (leave these files empty for now, they will be filled in later): -
Include them in your
lib.rs
module:
Implement Mocked Runtime¶
The following portion of code sets up a mock runtime (Test
) to test the custom-pallet
in an isolated environment. Using frame_support
macros, it defines a minimal runtime configuration with traits such as RuntimeCall
and RuntimeEvent
to simulate runtime behavior. The mock runtime integrates the System pallet
, which provides core functionality, and the custom pallet (pallet_custom
) under specific indices. Copy and paste the following snippet of code into your mock.rs
file:
use frame_support::{derive_impl, parameter_types};
use sp_runtime::BuildStorage;
type Block = frame_system::mocking::MockBlock<Test>;
#[frame_support::runtime]
mod runtime {
#[runtime::runtime]
#[runtime::derive(
RuntimeCall,
RuntimeEvent,
RuntimeError,
RuntimeOrigin,
RuntimeFreezeReason,
RuntimeHoldReason,
RuntimeSlashReason,
RuntimeLockId,
RuntimeTask
)]
pub struct Test;
#[runtime::pallet_index(0)]
pub type System = frame_system::Pallet<Test>;
#[runtime::pallet_index(1)]
pub type CustomPallet = pallet_custom::Pallet<Test>;
}
Once you have your mock runtime set up, you can customize it by implementing the configuration traits for the System pallet
and your custom-pallet
, along with additional constants and initial states for testing. Here's an example of how to extend the runtime configuration. Copy and paste the following snippet of code below the previous one you added to mock.rs
:
// System pallet configuration
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type Block = Block;
}
// Custom pallet configuration
parameter_types! {
pub const CounterMaxValue: u32 = 10;
}
impl pallet_custom::Config for Test {
type RuntimeEvent = RuntimeEvent;
type CounterMaxValue = CounterMaxValue;
}
// Test externalities initialization
pub fn new_test_ext() -> sp_io::TestExternalities {
frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap()
.into()
}
Explanation of the additions:
- System pallet configuration - implements the
frame_system::Config
trait for the mock runtime, setting up the basic system functionality and specifying the block type - Custom pallet configuration - defines the
Config
trait for thecustom-pallet
, including a constant (CounterMaxValue
) to set the maximum allowed counter value. In this case, that value is set to 10 for testing purposes - Test externalities initialization - the
new_test_ext()
function initializes the mock runtime with default configurations, creating a controlled environment for testing
Full Mocked Runtime¶
You can view the full mock.rs
implementation for the mock runtime here:
Complete mock.rs
use crate as pallet_custom;
use frame_support::{derive_impl, parameter_types};
use sp_runtime::BuildStorage;
type Block = frame_system::mocking::MockBlock<Test>;
#[frame_support::runtime]
mod runtime {
#[runtime::runtime]
#[runtime::derive(
RuntimeCall,
RuntimeEvent,
RuntimeError,
RuntimeOrigin,
RuntimeFreezeReason,
RuntimeHoldReason,
RuntimeSlashReason,
RuntimeLockId,
RuntimeTask
)]
pub struct Test;
#[runtime::pallet_index(0)]
pub type System = frame_system::Pallet<Test>;
#[runtime::pallet_index(1)]
pub type CustomPallet = pallet_custom::Pallet<Test>;
}
// System pallet configuration
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type Block = Block;
}
// Custom pallet configuration
parameter_types! {
pub const CounterMaxValue: u32 = 10;
}
impl pallet_custom::Config for Test {
type RuntimeEvent = RuntimeEvent;
type CounterMaxValue = CounterMaxValue;
}
// Test externalities initialization
pub fn new_test_ext() -> sp_io::TestExternalities {
frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap()
.into()
}
Implement Test Cases¶
Unit testing a pallet involves creating a comprehensive test suite that validates various scenarios. You ensure your pallet’s reliability, security, and expected behavior under different conditions by systematically testing successful operations, error handling, event emissions, state modifications, and access control.
As demonstrated in the previous tutorial, the pallet calls to be tested are as follows:
Custom pallet calls
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Set the value of the counter.
///
/// The dispatch origin of this call must be _Root_.
///
/// - `new_value`: The new value to set for the counter.
///
/// Emits `CounterValueSet` event when successful.
#[pallet::call_index(0)]
#[pallet::weight(0)]
pub fn set_counter_value(origin: OriginFor<T>, new_value: u32) -> DispatchResult {
ensure_root(origin)?;
ensure!(
new_value <= T::CounterMaxValue::get(),
Error::<T>::CounterValueExceedsMax
);
CounterValue::<T>::put(new_value);
Self::deposit_event(Event::<T>::CounterValueSet {
counter_value: new_value,
});
Ok(())
}
/// Increment the counter by a specified amount.
///
/// This function can be called by any signed account.
///
/// - `amount_to_increment`: The amount by which to increment the counter.
///
/// Emits `CounterIncremented` event when successful.
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn increment(origin: OriginFor<T>, amount_to_increment: u32) -> DispatchResult {
let who = ensure_signed(origin)?;
let current_value = CounterValue::<T>::get().unwrap_or(0);
let new_value = current_value
.checked_add(amount_to_increment)
.ok_or(Error::<T>::CounterOverflow)?;
ensure!(
new_value <= T::CounterMaxValue::get(),
Error::<T>::CounterValueExceedsMax
);
CounterValue::<T>::put(new_value);
UserInteractions::<T>::try_mutate(&who, |interactions| -> Result<_, Error<T>> {
let new_interactions = interactions
.unwrap_or(0)
.checked_add(1)
.ok_or(Error::<T>::UserInteractionOverflow)?;
*interactions = Some(new_interactions); // Store the new value
Ok(())
})?;
Self::deposit_event(Event::<T>::CounterIncremented {
counter_value: new_value,
who,
incremented_amount: amount_to_increment,
});
Ok(())
}
/// Decrement the counter by a specified amount.
///
/// This function can be called by any signed account.
///
/// - `amount_to_decrement`: The amount by which to decrement the counter.
///
/// Emits `CounterDecremented` event when successful.
#[pallet::call_index(2)]
#[pallet::weight(0)]
pub fn decrement(origin: OriginFor<T>, amount_to_decrement: u32) -> DispatchResult {
let who = ensure_signed(origin)?;
let current_value = CounterValue::<T>::get().unwrap_or(0);
let new_value = current_value
.checked_sub(amount_to_decrement)
.ok_or(Error::<T>::CounterValueBelowZero)?;
CounterValue::<T>::put(new_value);
UserInteractions::<T>::try_mutate(&who, |interactions| -> Result<_, Error<T>> {
let new_interactions = interactions
.unwrap_or(0)
.checked_add(1)
.ok_or(Error::<T>::UserInteractionOverflow)?;
*interactions = Some(new_interactions); // Store the new value
Ok(())
})?;
Self::deposit_event(Event::<T>::CounterDecremented {
counter_value: new_value,
who,
decremented_amount: amount_to_decrement,
});
Ok(())
}
}
The following sub-sections outline various scenarios in which the custom-pallet
can be tested. Feel free to add these snippets to your tests.rs
while you read the examples.
Successful Operations¶
Verify that the counter can be successfully incremented under normal conditions, ensuring the increment works and the correct event is emitted.
// Test successful counter increment
#[test]
fn it_works_for_increment() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Initialize the counter value to 0
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 0));
// Increment the counter by 5
assert_ok!(CustomPallet::increment(RuntimeOrigin::signed(1), 5));
// Check that the event emitted matches the increment operation
System::assert_last_event(Event::CounterIncremented {
counter_value: 5,
who: 1,
incremented_amount: 5
}.into());
});
}
Preventing Value Overflow¶
Test that the pallet prevents incrementing beyond the maximum allowed value, protecting against unintended state changes.
// Verify increment is blocked when it would exceed max value
#[test]
fn increment_fails_for_max_value_exceeded() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Set counter value close to max (10)
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 7));
// Ensure that incrementing by 4 exceeds max value (10) and fails
assert_noop!(
CustomPallet::increment(RuntimeOrigin::signed(1), 4),
Error::<Test>::CounterValueExceedsMax // Expecting CounterValueExceedsMax error
);
});
}
Origin and Access Control¶
Confirm that sensitive operations like setting counter value are restricted to authorized origins, preventing unauthorized modifications.
// Ensure non-root accounts cannot set counter value
#[test]
fn set_counter_value_fails_for_non_root() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Ensure only root (privileged account) can set counter value
assert_noop!(
CustomPallet::set_counter_value(RuntimeOrigin::signed(1), 5), // non-root account
sp_runtime::traits::BadOrigin // Expecting a BadOrigin error
);
});
}
Edge Case Handling¶
Ensure the pallet gracefully handles edge cases, such as preventing increment operations that would cause overflow.
// Ensure increment fails on u32 overflow
#[test]
fn increment_handles_overflow() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Set to max value
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 1));
assert_noop!(
CustomPallet::increment(RuntimeOrigin::signed(1), u32::MAX),
Error::<Test>::CounterOverflow
);
});
}
// Test successful counter decrement
Verifying State Changes¶
Test that pallet operations modify the internal state correctly and maintain expected storage values across different interactions.
#[test]
fn user_interactions_increment() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Initialize counter value to 0
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 0));
// Increment by 5 and decrement by 2
assert_ok!(CustomPallet::increment(RuntimeOrigin::signed(1), 5));
assert_ok!(CustomPallet::decrement(RuntimeOrigin::signed(1), 2));
// Check if the user interactions are correctly tracked
assert_eq!(UserInteractions::<Test>::get(1).unwrap_or(0), 2); // User should have 2 interactions
});
}
// Ensure user interactions prevent overflow
Full Test Suite¶
You can check the complete tests.rs
implementation for the Custom pallet here:
Complete tests.rs
use crate::{mock::*, Error, Event, UserInteractions};
use frame_support::{assert_noop, assert_ok};
// Verify root can successfully set counter value
#[test]
fn it_works_for_set_counter_value() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Set counter value within max allowed (10)
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 5));
// Ensure that the correct event is emitted when the value is set
System::assert_last_event(Event::CounterValueSet { counter_value: 5 }.into());
});
}
// Ensure non-root accounts cannot set counter value
#[test]
fn set_counter_value_fails_for_non_root() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Ensure only root (privileged account) can set counter value
assert_noop!(
CustomPallet::set_counter_value(RuntimeOrigin::signed(1), 5), // non-root account
sp_runtime::traits::BadOrigin // Expecting a BadOrigin error
);
});
}
// Check that setting value above max is prevented
#[test]
fn set_counter_value_fails_for_max_value_exceeded() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Ensure the counter value cannot be set above the max limit (10)
assert_noop!(
CustomPallet::set_counter_value(RuntimeOrigin::root(), 11),
Error::<Test>::CounterValueExceedsMax // Expecting CounterValueExceedsMax error
);
});
}
// Test successful counter increment
#[test]
fn it_works_for_increment() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Initialize the counter value to 0
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 0));
// Increment the counter by 5
assert_ok!(CustomPallet::increment(RuntimeOrigin::signed(1), 5));
// Check that the event emitted matches the increment operation
System::assert_last_event(Event::CounterIncremented {
counter_value: 5,
who: 1,
incremented_amount: 5
}.into());
});
}
// Verify increment is blocked when it would exceed max value
#[test]
fn increment_fails_for_max_value_exceeded() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Set counter value close to max (10)
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 7));
// Ensure that incrementing by 4 exceeds max value (10) and fails
assert_noop!(
CustomPallet::increment(RuntimeOrigin::signed(1), 4),
Error::<Test>::CounterValueExceedsMax // Expecting CounterValueExceedsMax error
);
});
}
// Ensure increment fails on u32 overflow
#[test]
fn increment_handles_overflow() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Set to max value
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 1));
assert_noop!(
CustomPallet::increment(RuntimeOrigin::signed(1), u32::MAX),
Error::<Test>::CounterOverflow
);
});
}
// Test successful counter decrement
#[test]
fn it_works_for_decrement() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Initialize counter value to 8
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 8));
// Decrement counter by 3
assert_ok!(CustomPallet::decrement(RuntimeOrigin::signed(1), 3));
// Ensure the event matches the decrement action
System::assert_last_event(Event::CounterDecremented {
counter_value: 5,
who: 1,
decremented_amount: 3
}.into());
});
}
// Verify decrement is blocked when it would go below zero
#[test]
fn decrement_fails_for_below_zero() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Set counter value to 5
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 5));
// Ensure that decrementing by 6 fails as it would result in a negative value
assert_noop!(
CustomPallet::decrement(RuntimeOrigin::signed(1), 6),
Error::<Test>::CounterValueBelowZero // Expecting CounterValueBelowZero error
);
});
}
// Check that user interactions are correctly tracked
#[test]
fn user_interactions_increment() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Initialize counter value to 0
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 0));
// Increment by 5 and decrement by 2
assert_ok!(CustomPallet::increment(RuntimeOrigin::signed(1), 5));
assert_ok!(CustomPallet::decrement(RuntimeOrigin::signed(1), 2));
// Check if the user interactions are correctly tracked
assert_eq!(UserInteractions::<Test>::get(1).unwrap_or(0), 2); // User should have 2 interactions
});
}
// Ensure user interactions prevent overflow
#[test]
fn user_interactions_overflow() {
new_test_ext().execute_with(|| {
System::set_block_number(1);
// Initialize counter value to 0
assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 0));
// Set user interactions to max value (u32::MAX)
UserInteractions::<Test>::insert(1, u32::MAX);
// Ensure that incrementing by 5 fails due to overflow in user interactions
assert_noop!(
CustomPallet::increment(RuntimeOrigin::signed(1), 5),
Error::<Test>::UserInteractionOverflow // Expecting UserInteractionOverflow error
);
});
}
Running Tests¶
Execute the test suite for your custom pallet using Cargo's test command. This will run all defined test cases and provide detailed output about the test results.
After running the test suite, you should see the following output in your terminal:
running 12 tests test mock::__construct_runtime_integrity_test::runtime_integrity_tests ... ok test mock::test_genesis_config_builds ... ok test test::set_counter_value_fails_for_max_value_exceeded ... ok test test::set_counter_value_fails_for_non_root ... ok test test::user_interactions_increment ... ok test test::it_works_for_increment ... ok test test::it_works_for_set_counter_value ... ok test test::it_works_for_decrement ... ok test test::increment_handles_overflow ... ok test test::decrement_fails_for_below_zero ... ok test test::increment_fails_for_max_value_exceeded ... ok test test::user_interactions_overflow ... ok test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s Doc-tests custom_pallet running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Where to Go Next¶
-
Tutorial Add Pallets to the Runtime
Enhance your runtime with custom functionality! Learn how to add, configure, and integrate pallets in Polkadot SDK-based blockchains.
| Created: December 12, 2024