原文作者: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 可以理解为"专门用于测试的最小运行时",它具备以下能力:
-
模拟链上状态:提供 storage/state 管理能力,允许你读写 Pallet 的存储项。
-
满足 Config trait 约束:为 Pallet 的 Config trait 提供必要的类型与常量实现。
-
隔离测试:不依赖外部网络、节点进程或复杂 runtime 组合,测试更专注、更快。
-
支持 Genesis 配置:可以在测试中快速设定初始状态,用于覆盖不同场景。
-
提升开发反馈速度:改代码 → 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/