Polkadot SDK Pallet 单元测试完整指南:从基础到实战

原文作者:PaperMoon团队

在 Polkadot SDK 中,单元测试(Unit Testing)是保障 Pallet 功能正确性和稳定性的核心手段。通过编写系统化的测试用例,可以验证 Pallet 中的函数是否按照预期运行,并确保相关的存储数据与事件在交互过程中被正确处理。

在本指南中,你将学习如何:

• 合理组织测试模块结构

• 测试可调度函数(Dispatchable Functions)

• 验证存储状态变更

• 校验事件是否正确触发

• 测试错误处理逻辑

• 在测试中使用创世配置(Genesis Configuration)

这些内容将帮助你建立完整的 Pallet 测试体系。

理解 FRAME 测试工具

FRAME(Framework for Runtime Aggregation of Modularized Entities)为 Pallet 测试提供了一套专业的宏与工具,极大提升了测试效率和可读性。

1. 常用断言宏(Assertion Macros)

FRAME 提供了多个高频使用的测试宏:

• assert_ok!:断言调度调用执行成功

• assert_noop!:断言调用失败且不产生状态变更

• assert_eq!:Rust 标准相等性断言

2. assert_noop! 说明

assert_noop! 用于验证:

• 调用失败

• 且不会修改任何链上状态

该宏在测试错误分支时尤为重要,可确保系统在异常情况下保持一致性。

System Pallet 测试辅助工具

frame_system Pallet 提供了多个测试辅助接口:

• System::events():获取当前测试中产生的全部事件

• System::assert_last_event():断言最后一个事件

• System::set_block_number():设置当前区块高度

事件与区块号说明

默认情况下,在区块高度为 0(创世区块)时不会触发事件。

如果测试涉及事件校验,必须先设置:

复制代码
cd pallets/pallet-custom/src

创建文件:

复制代码
touch tests.rs
  1. lib.rs 中引入测试模块

在 src/lib.rs 中添加:

复制代码
#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

完整示例:

复制代码
#![cfg_attr(not(feature = "std"), no_std)]

pub use pallet::*;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[frame::pallet]
pub mod pallet {
    // ...
}

配置测试模块结构

在 src/tests.rs 中添加基础结构:

复制代码
use crate::{mock::*, Error, Event};
use frame::deps::frame_support::{assert_noop, assert_ok};
use frame::deps::sp_runtime::DispatchError;

该配置主要用于:

• 引入 Mock Runtime

• 引入错误类型与事件类型

• 使用 FRAME 测试宏

• 测试权限错误(DispatchError)

完整 Pallet 代码参考

(以下代码为测试对象的完整实现,用于后续测试验证)

复制代码
#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

pub use pallet::*;

#[frame::pallet]
pub mod pallet {
    use alloc::vec::Vec;
    use frame::prelude::*;

    #[pallet::pallet]
    pub struct Pallet<T>(_);

    #[pallet::config]
    pub trait Config: frame_system::Config {
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

        #[pallet::constant]
        type CounterMaxValue: Get<u32>;
    }

    #[pallet::event]
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
    pub enum Event<T: Config> {
        CounterValueSet {
            new_value: u32,
        },
        CounterIncremented {
            new_value: u32,
            who: T::AccountId,
            amount: u32,
        },
        CounterDecremented {
            new_value: u32,
            who: T::AccountId,
            amount: u32,
        },
    }

    #[pallet::error]
    pub enum Error<T> {
        NoneValue,
        Overflow,
        Underflow,
        CounterMaxValueExceeded,
    }

    #[pallet::storage]
    pub type CounterValue<T> = StorageValue<_, u32, ValueQuery>;

    #[pallet::storage]
    pub type UserInteractions<T: Config> = StorageMap<
        _,
        Blake2_128Concat,
        T::AccountId,
        u32,
        ValueQuery
    >;

    #[pallet::genesis_config]
    #[derive(DefaultNoBound)]
    pub struct GenesisConfig<T: Config> {
        pub initial_counter_value: u32,
        pub initial_user_interactions: Vec<(T::AccountId, u32)>,
    }

    #[pallet::genesis_build]
    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
        fn build(&self) {
            CounterValue::<T>::put(self.initial_counter_value);
            for (account, count) in &self.initial_user_interactions {
                UserInteractions::<T>::insert(account, count);
            }
        }
    }

    #[pallet::call]
    impl<T: Config> Pallet<T> {
        #[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>::CounterMaxValueExceeded);
            CounterValue::<T>::put(new_value);
            Self::deposit_event(Event::CounterValueSet { new_value });
            Ok(())
        }

        #[pallet::call_index(1)]
        #[pallet::weight(0)]
        pub fn increment(origin: OriginFor<T>, amount: u32) -> DispatchResult {
            let who = ensure_signed(origin)?;
            let current_value = CounterValue::<T>::get();
            let new_value = current_value.checked_add(amount).ok_or(Error::<T>::Overflow)?;
            ensure!(new_value <= T::CounterMaxValue::get(), Error::<T>::CounterMaxValueExceeded);
            CounterValue::<T>::put(new_value);
            UserInteractions::<T>::mutate(&who, |count| {
                *count = count.saturating_add(1);
            });
            Self::deposit_event(Event::CounterIncremented { new_value, who, amount });
            Ok(())
        }

        #[pallet::call_index(2)]
        #[pallet::weight(0)]
        pub fn decrement(origin: OriginFor<T>, amount: u32) -> DispatchResult {
            let who = ensure_signed(origin)?;
            let current_value = CounterValue::<T>::get();
            let new_value = current_value.checked_sub(amount).ok_or(Error::<T>::Underflow)?;
            CounterValue::<T>::put(new_value);
            UserInteractions::<T>::mutate(&who, |count| {
                *count = count.saturating_add(1);
            });
            Self::deposit_event(Event::CounterDecremented { new_value, who, amount });
            Ok(())
        }
    }
}

编写第一个测试

下面我们从最基础的功能测试开始。

测试 Increment 基础功能

目标:验证 increment 函数是否正确更新状态并触发事件。

复制代码
#[test]
fn increment_works() {
    new_test_ext().execute_with(|| {
        System::set_block_number(1);

        let account = 1u64;

        assert_ok!(CustomPallet::increment(RuntimeOrigin::signed(account), 50));
        assert_eq!(crate::CounterValue::<Test>::get(), 50);

        System::assert_last_event(
            Event::CounterIncremented {
                new_value: 50,
                who: account,
                amount: 50,
            }
            .into(),
        );

        assert_eq!(crate::UserInteractions::<Test>::get(account), 1);
    });
}

运行测试:

复制代码
cargo test --package pallet-custom increment_works

输出结果:

复制代码
test tests::increment_works ... ok

说明测试通过。

测试错误场景

错误处理是区块链系统安全性的重要保障,必须重点测试。

  1. 溢出保护测试(Overflow)

    #[test]
    fn increment_fails_on_overflow() {
    new_test_ext_with_counter(u32::MAX).execute_with(|| {
    assert_noop!(
    CustomPallet::increment(RuntimeOrigin::signed(1), 1),
    Error::<Test>::Overflow
    );
    });
    }

验证数值溢出时系统能正确拒绝。

  1. 下溢保护测试(Underflow)

    #[test]
    fn decrement_fails_on_underflow() {
    new_test_ext_with_counter(10).execute_with(|| {
    assert_noop!(
    CustomPallet::decrement(RuntimeOrigin::signed(1), 11),
    Error::<Test>::Underflow
    );
    });
    }

用于防止负数状态产生。

测试访问控制

验证 Root 权限控制逻辑是否生效。

rust 复制代码
#[test]
fn set_counter_value_requires_root() {
    new_test_ext().execute_with(|| {
        let alice = 1u64;

        assert_noop!(
            CustomPallet::set_counter_value(RuntimeOrigin::signed(alice), 100),
            DispatchError::BadOrigin
        );

        assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 100));
        assert_eq!(crate::CounterValue::<Test>::get(), 100);
    });
}

该测试确保:

• 普通用户无法调用

• Root 用户可正常调用

测试事件触发

事件是区块链系统对外的重要信息接口,必须确保其准确性。

示例:

rust 复制代码
#[test]
fn set_counter_value_works() {
    new_test_ext().execute_with(|| {
        System::set_block_number(1);

        assert_ok!(CustomPallet::set_counter_value(RuntimeOrigin::root(), 100));
        assert_eq!(crate::CounterValue::<Test>::get(), 100);

        System::assert_last_event(Event::CounterValueSet { new_value: 100 }.into());
    });
}

测试要点:

  1. 设置区块高度

  2. 执行调用

  3. 校验最后事件

测试创世配置

验证 Genesis 配置是否正确初始化状态。

rust 复制代码
#[test]
fn genesis_config_works() {
    new_test_ext_with_interactions(42, vec![(1, 5), (2, 10)]).execute_with(|| {
        assert_eq!(crate::CounterValue::<Test>::get(), 42);

        assert_eq!(crate::UserInteractions::<Test>::get(1), 5);
        assert_eq!(crate::UserInteractions::<Test>::get(2), 10);
    });
}

该测试可确保链启动时状态正确。

运行全部测试

执行:

rust 复制代码
cargo test --package pallet-custom

示例输出:

rust 复制代码
test tests::increment_works ... ok
test tests::set_counter_value_works ... ok
...
test result: ok. 15 passed

说明所有测试通过。

Mock Runtime 自动测试说明

你会看到两个自动生成测试:

rust 复制代码
mock::__construct_runtime_integrity_test::runtime_integrity_tests
mock::test_genesis_config_builds

作用:

• 验证 Runtime 结构完整性

• 校验 Genesis 构建过程

这些测试用于保障测试环境本身的正确性。

总结

至此,你已经完成了一套完整的 Pallet 单元测试体系,覆盖了:

• 功能正确性验证

• 错误处理机制

• 权限控制

• 事件系统

• 状态变更

• 创世初始化

这些测试模式是 Polkadot Pallet 开发中的标准实践。

在开发更复杂业务逻辑时,建议始终遵循:

先设计测试,再实现功能(Test-Driven Development)

这样可以显著降低运行时风险,提高代码质量和可维护性。

原文链接:https://docs.polkadot.com/parachains/customize-runtime/pallet-development/pallet-testing/

相关推荐
devmoon7 小时前
为 Pallet 搭建最小化 Mock Runtime 并编写单元测试环境
开发语言·单元测试·区块链·智能合约·polkadot
晚霞的不甘8 小时前
Flutter for OpenHarmony 打造沉浸式呼吸引导应用:用动画疗愈身心
服务器·网络·flutter·架构·区块链
devmoon9 小时前
Chopsticks 本地分叉平行链实战指南
安全·智能合约·polkadot·erc-20·独立链
devmoon9 小时前
Polkadot SDK 自定义 Pallet Benchmark 指南:生成并接入 Weight
开发语言·网络·数据库·web3·区块链·波卡
综合热讯9 小时前
股票融资融券交易时间限制一览与制度说明
大数据·人工智能·区块链
暴躁小师兄数据学院10 小时前
【WEB3.0零基础转行笔记】Solidity编程篇-第一讲:简易存储
web3·区块链·智能合约
devmoon21 小时前
运行时(Runtime)是什么?为什么 Polkadot 的 Runtime 可以被“像搭积木一样”定制
开发语言·区块链·智能合约·polkadot·runtmie
暴躁小师兄数据学院1 天前
【WEB3.0零基础转行笔记】Rust编程篇-第一讲:课程简介
rust·web3·区块链·智能合约
devmoon1 天前
在 Paseo 测试网上获取 Coretime:On-demand 与 Bulk 的完整实操指南
开发语言·web3·区块链·测试用例·智能合约·solidity