diff --git a/.husky/pre-commit b/.husky/pre-commit index b5e852fa5..c1a1f9066 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -8,6 +8,7 @@ IGNORED_FILES=( ".snippets/code/develop/toolkit/parachains/spawn-chains/zombienet/write-tests/big-network-test.toml" ".snippets/code/develop/toolkit/parachains/spawn-chains/zombienet/write-tests/small-network-test.toml" ".snippets/code/develop/toolkit/parachains/spawn-chains/zombienet/write-tests/spawn-a-basic-chain-test.toml" + ".snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/cargo-dev-dependencies.toml" ) # Normalize ignored files pattern diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/Cargo.toml b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/Cargo.toml deleted file mode 100644 index c95081343..000000000 --- a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "custom-pallet" -version = "0.1.0" -license.workspace = true -authors.workspace = true -homepage.workspace = true -repository.workspace = true -edition.workspace = true - -[dependencies] -codec = { features = ["derive"], workspace = true } -scale-info = { features = ["derive"], workspace = true } -frame-support.workspace = true -frame-system.workspace = true - -[features] -default = ["std"] -std = ["codec/std", "frame-support/std", "frame-system/std", "scale-info/std"] diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/call_structure.rs b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/call_structure.rs deleted file mode 100644 index 8dea8d132..000000000 --- a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/call_structure.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[pallet::call] -impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight(0)] - pub fn set_counter_value(origin: OriginFor, new_value: u32) -> DispatchResult {} - - #[pallet::call_index(1)] - #[pallet::weight(0)] - pub fn increment(origin: OriginFor, amount_to_increment: u32) -> DispatchResult {} - - #[pallet::call_index(2)] - #[pallet::weight(0)] - pub fn decrement(origin: OriginFor, amount_to_decrement: u32) -> DispatchResult {} -} \ No newline at end of file diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/compilation-output.html b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/compilation-output.html deleted file mode 100644 index ddd0db1a7..000000000 --- a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/compilation-output.html +++ /dev/null @@ -1,5 +0,0 @@ -
- cargo build --release - Compiling solochain-template-node - Finished `release` profile [optimized] target(s) in 27.12s -
diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs deleted file mode 100644 index 13e4dec7b..000000000 --- a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs +++ /dev/null @@ -1,184 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -pub use pallet::*; - -#[frame_support::pallet(dev_mode)] -pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - #[pallet::pallet] - pub struct Pallet(_); - - // Configuration trait for the pallet - #[pallet::config] - pub trait Config: frame_system::Config { - // Defines the event type for the pallet - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - // Defines the maximum value the counter can hold - #[pallet::constant] - type CounterMaxValue: Get; - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// The counter value has been set to a new value by Root. - CounterValueSet { - /// The new value set. - counter_value: u32, - }, - /// A user has successfully incremented the counter. - CounterIncremented { - /// The new value set. - counter_value: u32, - /// The account who incremented the counter. - who: T::AccountId, - /// The amount by which the counter was incremented. - incremented_amount: u32, - }, - /// A user has successfully decremented the counter. - CounterDecremented { - /// The new value set. - counter_value: u32, - /// The account who decremented the counter. - who: T::AccountId, - /// The amount by which the counter was decremented. - decremented_amount: u32, - }, - } - - /// Storage for the current value of the counter. - #[pallet::storage] - pub type CounterValue = StorageValue<_, u32>; - - /// Storage map to track the number of interactions performed by each account. - #[pallet::storage] - pub type UserInteractions = StorageMap<_, Twox64Concat, T::AccountId, u32>; - - #[pallet::error] - pub enum Error { - /// The counter value exceeds the maximum allowed value. - CounterValueExceedsMax, - /// The counter value cannot be decremented below zero. - CounterValueBelowZero, - /// Overflow occurred in the counter. - CounterOverflow, - /// Overflow occurred in user interactions. - UserInteractionOverflow, - } - - #[pallet::call] - impl Pallet { - /// 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, new_value: u32) -> DispatchResult { - ensure_root(origin)?; - - ensure!( - new_value <= T::CounterMaxValue::get(), - Error::::CounterValueExceedsMax - ); - - CounterValue::::put(new_value); - - Self::deposit_event(Event::::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, amount_to_increment: u32) -> DispatchResult { - let who = ensure_signed(origin)?; - - let current_value = CounterValue::::get().unwrap_or(0); - - let new_value = current_value - .checked_add(amount_to_increment) - .ok_or(Error::::CounterOverflow)?; - - ensure!( - new_value <= T::CounterMaxValue::get(), - Error::::CounterValueExceedsMax - ); - - CounterValue::::put(new_value); - - UserInteractions::::try_mutate(&who, |interactions| -> Result<_, Error> { - let new_interactions = interactions - .unwrap_or(0) - .checked_add(1) - .ok_or(Error::::UserInteractionOverflow)?; - *interactions = Some(new_interactions); // Store the new value - - Ok(()) - })?; - - Self::deposit_event(Event::::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, amount_to_decrement: u32) -> DispatchResult { - let who = ensure_signed(origin)?; - - let current_value = CounterValue::::get().unwrap_or(0); - - let new_value = current_value - .checked_sub(amount_to_decrement) - .ok_or(Error::::CounterValueBelowZero)?; - - CounterValue::::put(new_value); - - UserInteractions::::try_mutate(&who, |interactions| -> Result<_, Error> { - let new_interactions = interactions - .unwrap_or(0) - .checked_add(1) - .ok_or(Error::::UserInteractionOverflow)?; - *interactions = Some(new_interactions); // Store the new value - - Ok(()) - })?; - - Self::deposit_event(Event::::CounterDecremented { - counter_value: new_value, - who, - decremented_amount: amount_to_decrement, - }); - - Ok(()) - } - } -} diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/scaffold.rs b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/scaffold.rs deleted file mode 100644 index 44ef5b701..000000000 --- a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/scaffold.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -pub use pallet::*; - -#[frame_support::pallet(dev_mode)] -pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - #[pallet::pallet] - pub struct Pallet(_); - - #[pallet::config] - pub trait Config: frame_system::Config {} -} \ No newline at end of file diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/Cargo-dev-dependencies.toml b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/Cargo-dev-dependencies.toml new file mode 100644 index 000000000..dd1df5623 --- /dev/null +++ b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/Cargo-dev-dependencies.toml @@ -0,0 +1,10 @@ +[dependencies] +... + +[dev-dependencies] +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } + +[features] +... \ No newline at end of file diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/mock.rs b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/mock.rs new file mode 100644 index 000000000..ac39e24cb --- /dev/null +++ b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/mock.rs @@ -0,0 +1,52 @@ +use crate as pallet_custom; +use frame_support::{derive_impl, parameter_types}; +use sp_runtime::BuildStorage; + +type Block = frame_system::mocking::MockBlock; + +#[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; + + #[runtime::pallet_index(1)] + pub type CustomPallet = pallet_custom::Pallet; +} + +// 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::::default() + .build_storage() + .unwrap() + .into() +} \ No newline at end of file diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/output.html b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/output.html new file mode 100644 index 000000000..bbf99097d --- /dev/null +++ b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/output.html @@ -0,0 +1,24 @@ +
+ cargo test --package custom-pallet +
+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
+    
+
diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/pallet-calls.rs b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/pallet-calls.rs new file mode 100644 index 000000000..a3bd676fc --- /dev/null +++ b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/pallet-calls.rs @@ -0,0 +1,111 @@ +#[pallet::call] +impl Pallet { + /// 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, new_value: u32) -> DispatchResult { + ensure_root(origin)?; + + ensure!( + new_value <= T::CounterMaxValue::get(), + Error::::CounterValueExceedsMax + ); + + CounterValue::::put(new_value); + + Self::deposit_event(Event::::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, amount_to_increment: u32) -> DispatchResult { + let who = ensure_signed(origin)?; + + let current_value = CounterValue::::get().unwrap_or(0); + + let new_value = current_value + .checked_add(amount_to_increment) + .ok_or(Error::::CounterOverflow)?; + + ensure!( + new_value <= T::CounterMaxValue::get(), + Error::::CounterValueExceedsMax + ); + + CounterValue::::put(new_value); + + UserInteractions::::try_mutate(&who, |interactions| -> Result<_, Error> { + let new_interactions = interactions + .unwrap_or(0) + .checked_add(1) + .ok_or(Error::::UserInteractionOverflow)?; + *interactions = Some(new_interactions); // Store the new value + + Ok(()) + })?; + + Self::deposit_event(Event::::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, amount_to_decrement: u32) -> DispatchResult { + let who = ensure_signed(origin)?; + + let current_value = CounterValue::::get().unwrap_or(0); + + let new_value = current_value + .checked_sub(amount_to_decrement) + .ok_or(Error::::CounterValueBelowZero)?; + + CounterValue::::put(new_value); + + UserInteractions::::try_mutate(&who, |interactions| -> Result<_, Error> { + let new_interactions = interactions + .unwrap_or(0) + .checked_add(1) + .ok_or(Error::::UserInteractionOverflow)?; + *interactions = Some(new_interactions); // Store the new value + + Ok(()) + })?; + + Self::deposit_event(Event::::CounterDecremented { + counter_value: new_value, + who, + decremented_amount: amount_to_decrement, + }); + + Ok(()) + } +} \ No newline at end of file diff --git a/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs new file mode 100644 index 000000000..00fffdb06 --- /dev/null +++ b/.snippets/code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs @@ -0,0 +1,157 @@ +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::::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::::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::::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::::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::::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::::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::::UserInteractionOverflow // Expecting UserInteractionOverflow error + ); + }); +} diff --git a/tutorials/polkadot-sdk/parachains/build-custom-pallet/.pages b/tutorials/polkadot-sdk/parachains/build-custom-pallet/.pages index 302d5ae07..a30ef0372 100644 --- a/tutorials/polkadot-sdk/parachains/build-custom-pallet/.pages +++ b/tutorials/polkadot-sdk/parachains/build-custom-pallet/.pages @@ -1,4 +1,5 @@ title: Build Custom Pallet nav: - index.md - - 'Build Pallet': build-pallet.md + - 'Pallet Unit Testing': pallet-unit-testing.md + \ No newline at end of file diff --git a/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet.md b/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet.md deleted file mode 100644 index 2d55b437f..000000000 --- a/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet.md +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: Build the Pallet -description: Learn how to build a custom pallet for Polkadot SDK-based blockchains with this step-by-step guide. Create and configure a simple counter pallet from scratch. ---- - -# Build the Pallet - -## Introduction - -In Polkadot SDK-based blockchains, runtime functionality is built through modular components called [pallets](/polkadot-protocol/glossary#pallet){target=\_blank}. These pallets are Rust-based runtime modules created using [FRAME (Framework for Runtime Aggregation of Modular Entities)](/develop/parachains/customize-parachain/overview/){target=\_blank}, a powerful library that simplifies blockchain development by providing specialized macros and standardized patterns for building blockchain logic. -A pallet encapsulates a specific set of blockchain functionalities, such as managing token balances, implementing governance mechanisms, or creating custom state transitions. - -In this tutorial, you'll learn how to create a custom pallet from scratch. You will develop a simple counter pallet with the following features: - -- Users can increment and decrement a counter -- Only a [root origin](https://paritytech.github.io/polkadot-sdk/master/frame_system/pallet/type.Origin.html#variant.Root){target=\_blank} can set an arbitrary counter value - -You'll use the [Polkadot SDK Solochain Template](https://github.com/paritytech/polkadot-sdk-solochain-template){target=\_blank}, a pre-configured blockchain template that provides a functional development environment. - -## Prerequisites - -To set up your development environment for the Polkadot SDK, you'll need: - -- **Rust installed** - the node template is written in [Rust](https://www.rust-lang.org/){target=\_blank}. Install it by following the [Installation](/develop/parachains/get-started/install-polkadot-sdk){target=\_blank} guide for step-by-step instructions on setting up your development environment - -## Set Up a Chain Template - -The [Polkadot SDK Solochain Template](https://github.com/paritytech/polkadot-sdk-solochain-template){target=\_blank} provides a ready-to-use development environment for building using the [Polkadot SDK](https://github.com/paritytech/polkadot-sdk){target=\_blank}. Follow these steps to compile the node: - -1. Clone the repository: - ```bash - git clone -b {{dependencies.polkadot_sdk_solochain_template.version}} {{dependencies.polkadot_sdk_solochain_template.repository_url}} - ``` - - !!!note - Ensure you're using the version `{{dependencies.polkadot_sdk_solochain_template.version}}` of the Polkadot SDK Solochain Template to be able to follow this tutorial step by step. - -2. Navigate to the project directory: - ```bash - cd polkadot-sdk-solochain-template - ``` - -3. Compile the node template: - ```bash - cargo build --release - ``` - - !!!note - Depending on your machine's specifications, initial compilation may take several minutes. For optimized artifacts, use the `--release` flag. - -4. Verify successful compilation by checking that the output is similar to: - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/compilation-output.html' - -## Create a New Project - -In this tutorial, you'll build a custom pallet from scratch to demonstrate the complete workflow, rather than starting with the pre-built `pallet-template`. The first step is to create a new Rust package for your pallet: - -1. Navigate to the `pallets` directory in your workspace: - - ```bash - cd pallets - ``` - -2. Create a new Rust library project for your custom pallet by running the following command: - - ```bash - cargo new --lib custom-pallet - ``` - -3. Enter the new project directory: - - ```bash - cd custom-pallet - ``` - -4. Ensure the project was created successfully by checking its structure. The file layout should resemble the following: - - ``` - custom-pallet - ├── Cargo.toml - └── src - └── lib.rs - ``` - - If the files are in place, your project setup is complete, and you're ready to start building your custom pallet. - -## Add Dependencies - -To build and integrate your custom pallet into a Polkadot SDK-based runtime, you must add specific dependencies to the `Cargo.toml` file of your pallet's project. These dependencies provide essential modules and features required for pallet development. Since your custom pallet is part of a workspace that includes other components, such as the runtime, the configuration must align with the workspace structure. Follow the steps below to set up your `Cargo.toml` file properly: - -1. Open your `Cargo.toml` file - -2. Add the required dependencies in the `[dependencies]` section: - - ```toml - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/Cargo.toml:10:14' - ``` - -3. Enable `std` features: - - ```toml - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/Cargo.toml:16:18' - ``` - -The final `Cargo.toml` should resemble the following: - -??? note "Complete `Cargo.toml` File" - - ```toml - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/Cargo.toml' - ``` - -## Implement the Pallet Logic - -In this section, you will construct the core structure of your custom pallet, starting with setting up its basic scaffold. This scaffold acts as the foundation, enabling you to later add functionality such as storage items, events, errors, and dispatchable calls. - -### Add Scaffold Pallet Structure - -You now have the bare minimum of package dependencies that your pallet requires specified in the `Cargo.toml` file. The next step is to prepare the scaffolding for your new pallet. - -1. Open `src/lib.rs` in a text editor and delete all the content - -2. Prepare the scaffolding for the pallet by adding the following: - - ```rust - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/scaffold.rs' - ``` - -3. Verify that it compiles by running the following command: - - ```bash - cargo build --package custom-pallet - ``` - -### Pallet Configuration - -Implementing the `#[pallet::config]` macro is mandatory and sets the module's dependency on other modules and the types and values specified by the runtime-specific settings. - -In this step, you will configure two essential components that are critical for the pallet's functionality: - -- **`RuntimeEvent`** - since this pallet emits events, the [`RuntimeEvent`](https://paritytech.github.io/polkadot-sdk/master/frame_system/pallet/trait.Config.html#associatedtype.RuntimeEvent){target=\_blank} type is required to handle them. This ensures that events generated by the pallet can be correctly processed and interpreted by the runtime - -- **`CounterMaxValue`** - a constant that sets an upper limit on the value of the counter, ensuring that the counter remains within a predefined range - -Add the following `Config` trait definition to your pallet: - -```rust ---8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs:14:23' -``` - -### Add Events - -Events allow the pallet to communicate with the outside world by emitting signals when specific actions occur. These events are critical for transparency, debugging, and integration with external systems such as UIs or monitoring tools. - -Below are the events defined for this pallet: - -- **`CounterValueSet`** - is emitted when the counter is explicitly set to a new value. This event includes the counter's updated value - -- **`CounterIncremented`** - is emitted after a successful increment operation. It includes: - - - The new counter value - - The account responsible for the increment - - The amount by which the counter was incremented - -- **`CounterDecremented`** - is emitted after a successful decrement operation. It includes: - - - The new counter value - - The account responsible for the decrement - - The amount by which the counter was decremented - -Define the events in the pallet as follows: - -```rust ---8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs:25:51' -``` - -### Add Storage Items - -Storage items are used to manage the pallet's state. This pallet defines two items to handle the counter's state and user interactions: - -- **`CounterValue`** - a single storage value that keeps track of the current value of the counter. This value is the core state variable manipulated by the pallet's functions - -- **`UserInteractions`** - a storage map that tracks the number of times each account interacts with the counter - -Define the storage items as follows: - -```rust ---8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs:53:59' -``` - -### Implement Custom Errors - -The `#[pallet::error]` macro defines a custom `Error` enum to handle specific failure conditions within the pallet. Errors help provide meaningful feedback to users and external systems when an extrinsic cannot be completed successfully. They are critical for maintaining the pallet's clarity and robustness. - -To add custom errors, use the `#[pallet::error]` macro to define the `Error` enum. Each variant represents a unique error that the pallet can emit, and these errors should align with the logic and constraints of the pallet. - -Add the following errors to the pallet: - -```rust ---8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs:61:71' -``` - -### Implement Calls - -The `#[pallet::call]` macro defines the dispatchable functions (or calls) the pallet exposes. These functions allow users or the runtime to interact with the pallet's logic and state. Each call includes comprehensive validations, modifies the state, and optionally emits events to signal successful execution. - -The structure of the dispatchable calls in this pallet is as follows: - -```rust ---8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/call_structure.rs' -``` - -Below you can find the implementations of each dispatchable call in this pallet: - -???- function "set_counter_value(origin: OriginFor, new_value: u32) -> DispatchResult" - This call sets the counter to a specific value. It is restricted to the Root origin, meaning it can only be invoked by privileged users or entities. - - - **Parameters**: - - `new_value` - the value to set the counter to - - **Validations**: - - The new value must not exceed the maximum allowed counter value (`CounterMaxValue`) - - **Behavior**: - - Updates the `CounterValue` storage item - - Emits a `CounterValueSet` event on success - - ```rust - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs:75:99' - ``` - -???- function "increment(origin: OriginFor, amount_to_increment: u32) -> DispatchResult" - This call increments the counter by a specified amount. It is accessible to any signed account. - - - **Parameters**: - - `amount_to_increment` - the amount to add to the counter - - **Validations**: - - Prevents overflow during the addition - - Ensures the resulting counter value does not exceed `CounterMaxValue` - - **Behavior**: - - Updates the `CounterValue` storage item - - Tracks the number of interactions by the user in the `UserInteractions` storage map - - Emits a `CounterIncremented` event on success - - ```rust - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs:101:143' - ``` - - -???- function "decrement(origin: OriginFor, amount_to_decrement: u32) -> DispatchResult" - This call decrements the counter by a specified amount. It is accessible to any signed account. - - - **Parameters**: - - `amount_to_decrement` - the amount to subtract from the counter - - **Validations**: - - Prevents underflow during the subtraction - - Ensures the counter does not drop below zero - - **Behavior**: - - Updates the `CounterValue` storage item - - Tracks the number of interactions by the user in the `UserInteractions` storage map - - Emits a `CounterDecremented` event on success - - ```rust - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs:145:182' - ``` - -## Verify Compilation - -After implementing all the pallet components, verifying that the code still compiles successfully is crucial. Run the following command in your terminal to ensure there are no errors: - -```bash -cargo build --package custom-pallet -``` - -If you encounter any errors or warnings, carefully review your code to resolve the issues. Once the build is complete without errors, your pallet implementation is ready. - -## Key Takeaways - -In this tutorial, you learned how to create a custom pallet by defining storage, implementing errors, adding dispatchable calls, and emitting events. These are the foundational building blocks for developing robust Polkadot SDK-based blockchain logic. - -To review this implementation, you can find the complete pallet code below: - -???+ example "Complete Pallet Code" - ```rust - --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/build-pallet/lib.rs' - ``` - -## Where to Go Next - -To ensure the reliability of your pallet, it’s crucial to test its functionality thoroughly. - -Proceed to the following tutorial: [Pallet Testing](TODO: add-path), where you’ll learn how to write comprehensive tests for your pallet to validate its behavior and prevent bugs. \ No newline at end of file diff --git a/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing.md b/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing.md new file mode 100644 index 000000000..bdcfe4245 --- /dev/null +++ b/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing.md @@ -0,0 +1,165 @@ +--- +title: Pallet Unit Testing +description: Discover how to create thorough unit tests for pallets built with the Polkadot SDK, using a custom pallet as a practical example. +--- + +# Pallet Unit Testing + +## Introduction + +You have learned how to create a custom pallet in the [Build the Pallet](/tutorials/polkadot-sdk/parachains/build-custom-pallet/){target=\_blank} tutorial; now you will see how to test the pallet to ensure that it works as expected. As stated in the [Pallet Testing](/develop/parachains/customize-parachain/pallet-testing/){target=\_blank} 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 built in the [previous tutorial](/tutorials/polkadot-sdk/parachains/build-custom-pallet/){target=\_blank}, covering essential testing aspects and steps. + +## Prerequisites + +To set up your testing environment for Polkadot SDK pallets, you'll need: + +- [Polkadot SDK dependencies](/develop/parachains/get-started/install-polkadot-sdk/){target=\_blank} installed +- Basic understanding of Substrate/Polkadot SDK concepts +- A custom pallet implementation, check the [Build the Pallet](/tutorials/polkadot-sdk/parachains/build-custom-pallet/){target=\_blank} tutorial +- Familiarity with [Rust testing frameworks](https://doc.rust-lang.org/book/ch11-01-writing-tests.html){target=\_blank} + +## Set Up the Testing Environment + +To effectively create the test environment for your pallet, you'll need to follow these steps: + +1. Move to the project directory + + ```bash + cd custom-pallet + ``` + +2. Add the required dependencies to your test configuration in the `Cargo.toml` file of the pallet: + + ```toml + --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/cargo-dev-dependencies.toml' + ``` + +3. Create a `mock.rs` and a `tests.rs` files (leave these files empty for now, they will be filled in later): + + ```bash + touch src/mock.rs + touch src/tests.rs + ``` + +4. Include them in your `lib.rs` module: + + ```rust + #[cfg(test)] + mod mock; + + #[cfg(test)] + mod tests; + ``` + +## 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`](https://paritytech.github.io/polkadot-sdk/master/frame_support/index.html){target=\_blank} 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`](https://paritytech.github.io/polkadot-sdk/master/frame_system/index.html){target=\_blank}, 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: + +```rust +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/mock.rs:1:29' +``` + +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`: + +```rust +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/mock.rs:30:52' +``` + +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 the `custom-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`" + + ```rust + --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/mock.rs' + ``` + +## 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" + + ```rust + --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/pallet-calls.rs' + ``` + +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. + +```rust +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs:42:60' +``` + +### Preventing Value Overflow + +Test that the pallet prevents incrementing beyond the maximum allowed value, protecting against unintended state changes. + +```rust +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs:61:75' +``` + +### Origin and Access Control + +Confirm that sensitive operations like setting counter value are restricted to authorized origins, preventing unauthorized modifications. + +```rust +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs:16:28' +``` + +### Edge Case Handling + +Ensure the pallet gracefully handles edge cases, such as preventing increment operations that would cause overflow. + +```rust +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs:76:90' +``` + +### Verifying State Changes + +Test that pallet operations modify the internal state correctly and maintain expected storage values across different interactions. + +```rust +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs:125:141' +``` + +### Full Test Suite + +You can check the complete `tests.rs` implementation for the Custom pallet here: + +???- "Complete `tests.rs`" + + ```rust + --8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/tests.rs' + ``` + + +## 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. + +```bash +cargo test --package custom-pallet +``` + +After running the test suite, you should see the following output in your terminal: + +--8<-- 'code/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-unit-testing/output.html' + +## Where to Go Next + +Expand your Polkadot SDK development skills by exploring the [`Pallet Benchmarking`](/tutorials/polkadot-sdk/parachains/build-custom-pallet/pallet-benchmarking){target=\_blank} tutorial.