为 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/

相关推荐
Scott9999HH2 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海3 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
马优晨5 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区7 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大7 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜7 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa8 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio