Skip to content

Pallet Testing

Introduction

Unit testing in the Polkadot SDK helps ensure that the functions provided by a pallet behave as expected. It also confirms that data and events associated with a pallet are processed correctly during interactions. The Polkadot SDK offers a set of APIs to create a test environment to simulate runtime and mock transaction execution for extrinsics and queries.

To begin unit testing, you must first set up a mock runtime that simulates blockchain behavior, incorporating the necessary pallets. For a deeper understanding, consult the Mock Runtime guide.

Writing Unit Tests

Once the mock runtime is in place, the next step is to write unit tests that evaluate the functionality of your pallet. Unit tests allow you to test specific pallet features in isolation, ensuring that each function behaves correctly under various conditions. These tests typically reside in your pallet module's test.rs file.

Unit tests in the Polkadot SDK use the Rust testing framework, and the mock runtime you've defined earlier will serve as the test environment. Below are the typical steps involved in writing unit tests for a pallet.

The tests confirm that:

  • Pallets initialize correctly - at the start of each test, the system should initialize with block number 0, and the pallets should be in their default states
  • Pallets modify each other's state - the second test shows how one pallet can trigger changes in another pallet's internal state, confirming proper cross-pallet interactions
  • State transitions between blocks are seamless - by simulating block transitions, the tests validate that the runtime responds correctly to changes in the block number

Testing pallet interactions within the runtime is critical for ensuring the blockchain behaves as expected under real-world conditions. Writing integration tests allows validation of how pallets function together, preventing issues that might arise when the system is fully assembled.

This approach provides a comprehensive view of the runtime's functionality, ensuring the blockchain is stable and reliable.

Test Initialization

Each test starts by initializing the runtime environment, typically using the new_test_ext() function, which sets up the mock storage and environment.

#[test]
fn test_pallet_functionality() {
    new_test_ext().execute_with(|| {
        // Test logic goes here
    });
}

Function Call Testing

Call the pallet's extrinsics or functions to simulate user interaction or internal logic. Use the assert_ok! macro to check for successful execution and assert_err! to verify that errors are correctly handled.

#[test]
fn it_works_for_valid_input() {
    new_test_ext().execute_with(|| {
        // Call an extrinsic or function
        assert_ok!(TemplateModule::some_function(Origin::signed(1), valid_param));
    });
}

#[test]
fn it_fails_for_invalid_input() {
    new_test_ext().execute_with(|| {
        // Call an extrinsic with invalid input and expect an error
        assert_err!(
            TemplateModule::some_function(Origin::signed(1), invalid_param),
            Error::<Test>::InvalidInput
        );
    });
}

Storage Testing

After calling a function or extrinsic in your pallet, it's essential to verify that the state changes in the pallet's storage match the expected behavior to ensure data is updated correctly based on the actions taken.

The following example shows how to test the storage behavior before and after the function call:

#[test]
fn test_storage_update_on_extrinsic_call() {
    new_test_ext().execute_with(|| {
        // Check the initial storage state (before the call)
        assert_eq!(Something::<Test>::get(), None);

        // Dispatch a signed extrinsic, which modifies storage
        assert_ok!(TemplateModule::do_something(RuntimeOrigin::signed(1), 42));

        // Validate that the storage has been updated as expected (after the call)
        assert_eq!(Something::<Test>::get(), Some(42));
    });
}

Event Testing

It's also crucial to test the events that your pallet emits during execution. By default, events generated in a pallet using the #generate_deposit macro are stored under the system's event storage key (system/events) as EventRecord entries. These can be accessed using System::events() or verified with specific helper methods provided by the system pallet, such as assert_has_event and assert_last_event.

Here's an example of testing events in a mock runtime:

#[test]
fn it_emits_events_on_success() {
    new_test_ext().execute_with(|| {
        // Call an extrinsic or function
        assert_ok!(TemplateModule::some_function(Origin::signed(1), valid_param));

        // Verify that the expected event was emitted
        assert!(System::events().iter().any(|record| {
            record.event == Event::TemplateModule(TemplateEvent::SomeEvent)
        }));
    });
}

Some key considerations are:

  • Block number - events are not emitted on the genesis block, so you need to set the block number using System::set_block_number() to ensure events are triggered
  • Converting events - use .into() when instantiating your pallet's event to convert it into a generic event type, as required by the system's event storage

Where to Go Next

  • Guide Benchmarking


    Explore methods to measure the performance and execution cost of your pallet.

    Reference

Last update: December 13, 2024
| Created: December 13, 2024