为 Pallet 搭建最小化 Mock Runtime 并编写单元测试环境

原文作者:PaperMoon团队

在 Pallet 开发过程中,测试是必不可少的一环。通常我们不会一上来就把 Pallet 集成进完整的 Runtime 并启动节点来验证逻辑,因为这会带来较高的编译、运行和调试成本。

更高效的做法是先为 Pallet 构建一个 Mock Runtime:它是一个最小化、可模拟链上环境的测试运行时,用于在不启动完整节点的情况下,对 Pallet 的关键逻辑进行隔离验证。

通过本指南,你将创建 Mock Runtime,从而支持编写覆盖全面的单元测试,验证包括但不限于:

• Dispatchable(可调用函数 / extrinsics)的行为是否符合预期

• Storage(链上存储)是否按逻辑发生变化

• Event(事件)是否正确发出

• Error(错误)是否在适当场景返回

• 权限控制、origin 校验是否正确(signed / root 等)

• Genesis 配置是否能正确初始化存储状态

前置条件(Prerequisites)

开始之前请确认:

• 已完成「Make a Custom Pallet」教程

• 已在 pallets/pallet-custom 目录中拥有该自定义计数器 Pallet

• 对 Rust 测试(cargo test、#[cfg(test)]、单元测试结构)有基础理解

理解 Mock Runtime

Mock Runtime 可以理解为"专门用于测试的最小运行时",它具备以下能力:

  1. 模拟链上状态:提供 storage/state 管理能力,允许你读写 Pallet 的存储项。

  2. 满足 Config trait 约束:为 Pallet 的 Config trait 提供必要的类型与常量实现。

  3. 隔离测试:不依赖外部网络、节点进程或复杂 runtime 组合,测试更专注、更快。

  4. 支持 Genesis 配置:可以在测试中快速设定初始状态,用于覆盖不同场景。

  5. 提升开发反馈速度:改代码 → cargo test → 立即验证逻辑,适合 TDD/快速迭代。

需要强调:Mock Runtime 只用于本地测试,不会被部署到线上链上环境中。

创建 Mock Runtime 模块(Create the Mock Runtime Module)

1)进入 Pallet 源码目录

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

2)创建 mock.rs

复制代码
touch mock.rs

3)在 src/lib.rs 中声明 mock 模块

在 pub use pallet::*; 之后添加:

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

pub use pallet::*;

#[cfg(test)]
mod mock;

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

#[cfg(test)] 的作用是:只有在执行测试(如 cargo test)时才编译该模块,避免影响正常编译产物。

编写 Mock Runtime 基础结构

打开 src/mock.rs,添加必要依赖与类型定义:

rust 复制代码
use crate as pallet_custom;
use frame::{
    deps::{
        frame_support::{ derive_impl, traits::ConstU32 },
        sp_io,
        sp_runtime::{ traits::IdentityLookup, BuildStorage },
    },
    prelude::*,
};

type Block = frame_system::mocking::MockBlock<Test>;

// Configure a mock runtime to test the pallet.
frame::deps::frame_support::construct_runtime!(
    pub enum Test
    {
        System: frame_system,
        CustomPallet: pallet_custom,
    }
);

这段代码的关键点:

• construct_runtime!:构建一个最小 Runtime,只包含测试所需的 Pallet

• Test:测试用 runtime 类型

• Block:mock block 类型别名,让 frame_system 能在测试中运行

配置 frame_system

frame_system 是所有 Pallet 的基础依赖,它提供了账户、origin、事件、区块信息等核心能力。这里我们用简化方式为测试环境配置它:

rust 复制代码
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
    type Block = Block;
    type AccountId = u64;
    type Lookup = IdentityLookup<Self::AccountId>;
}

配置解释:

• #[derive_impl(...TestDefaultConfig)]:自动为大部分类型填充适合测试的默认配置,减少样板代码

• AccountId = u64:测试中用整数当账户 ID,避免引入复杂的加密账户类型

• Lookup = IdentityLookup:账户映射直接等同,不做地址转换

• Block = Block:使用上面定义的 mock block

这种写法比手动指定全部 frame_system::Config 关联类型更简洁,适合单元测试场景。

实现自定义 Pallet 的 Config

为你的 pallet_custom::Config 在 mock runtime 中提供实现:

rust 复制代码
impl pallet_custom::Config for Test {
    type RuntimeEvent = RuntimeEvent;
    type CounterMaxValue = ConstU32<1000>;
}

说明:

• RuntimeEvent:把 Pallet 事件接入 mock runtime 的事件系统

• CounterMaxValue = 1000:设置最大计数值为 1000,通常建议与生产环境保持一致,除非测试需要刻意修改

配置 Genesis Storage

Genesis Storage 用于定义链在尚未出块前的初始状态。由于你的计数器 Pallet 已支持 genesis 配置,因此可以在测试中快速创建不同初始状态,覆盖更多边界场景。

1)基础测试环境:默认 genesis

rust 复制代码
pub fn new_test_ext() -> sp_io::TestExternalities {
    let mut t = frame_system::GenesisConfig::<Test>::default()
        .build_storage()
        .unwrap();

    (pallet_custom::GenesisConfig::<Test> {
        initial_counter_value: 0,
        initial_user_interactions: vec![],
    })
    .assimilate_storage(&mut t)
    .unwrap();

    t.into()
}

它会构造一个干净的链上状态:

• 计数器初始值 0

• 用户交互记录为空

2)自定义 genesis:指定初始 counter 值

rust 复制代码
pub fn new_test_ext_with_counter(initial_value: u32) -> sp_io::TestExternalities {
    let mut t = frame_system::GenesisConfig::<Test>::default()
        .build_storage()
        .unwrap();

    (pallet_custom::GenesisConfig::<Test> {
        initial_counter_value: initial_value,
        initial_user_interactions: vec![],
    })
    .assimilate_storage(&mut t)
    .unwrap();

    t.into()
}

适用于测试:

• 初始值为某个边界值(例如 999、1000)

• 验证 increment 是否会触发 CounterMaxValueExceeded

3)自定义 genesis:预置用户交互次数

rust 复制代码
pub fn new_test_ext_with_interactions(
    initial_value: u32,
    interactions: Vec<(u64, u32)>
) -> sp_io::TestExternalities {
    let mut t = frame_system::GenesisConfig::<Test>::default()
        .build_storage()
        .unwrap();

    (pallet_custom::GenesisConfig::<Test> {
        initial_counter_value: initial_value,
        initial_user_interactions: interactions,
    })
    .assimilate_storage(&mut t)
    .unwrap();

    t.into()
}

适用于测试:

• 某账户在 genesis 就已经有交互计数

• 验证 extrinsic 调用后 UserInteractions 是否按逻辑叠加

关键方法说明

• BuildStorage::build_storage():生成初始 storage

• assimilate_storage(...):将某 Pallet 的 genesis 配置合并写入 storage

如果需要为多个 Pallet 配置 genesis,可以连续调用多次 assimilate_storage

验证 Mock Runtime 是否能编译

在开始写测试用例之前,先确保 mock runtime 能通过编译:

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

该命令会编译测试代码(包括 mock.rs 和 genesis 配置),但并不一定会执行具体测试逻辑。若出现编译错误,应先修复再继续。

总结

到这里,你已经成功为自定义 Pallet 构建了 Mock Runtime 与 Genesis 测试环境。接下来你可以:

• 在不集成完整 runtime、不启动节点的情况下验证 Pallet

• 使用不同的 genesis 配置快速构造测试场景

• 针对 extrinsic、storage、event、error、origin 等关键行为编写单元测试

• 在集成进 parachain runtime 前,提前发现并修复逻辑问题

Mock Runtime + Genesis 配置是 Pallet 测试驱动开发(TDD)的基础设施,能显著提升迭代速度与逻辑可靠性。

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

相关推荐
Coder_Boy_8 小时前
Java开发者破局指南:跳出内卷,借AI赋能,搭建系统化知识体系
java·开发语言·人工智能·spring boot·后端·spring
Mr_Xuhhh8 小时前
介绍一下ref
开发语言·c++·算法
nbsaas-boot8 小时前
软件开发最核心的理念:接口化与组件化
开发语言
lsx2024068 小时前
Java 对象概述
开发语言
晚霞的不甘8 小时前
Flutter for OpenHarmony 打造沉浸式呼吸引导应用:用动画疗愈身心
服务器·网络·flutter·架构·区块链
Mr_Xuhhh8 小时前
C++11实现线程池
开发语言·c++·算法
无水先生8 小时前
python函数的参数管理(01)*args和**kwargs
开发语言·python
py小王子8 小时前
dy评论数据爬取实战:基于DrissionPage的自动化采集方案
大数据·开发语言·python·毕业设计
小陶的学习笔记8 小时前
python~基础
开发语言·python·学习