单元测试

⚠️ Update Notice:

Please read Substrate to Polkadot SDK page first.


在构建运行时逻辑时,您需要定期测试逻辑是否按预期工作。 您可以使用 Rust 提供的 单元测试框架 为运行时创建单元测试。 创建了一个或多个单元测试后,您可以使用 cargo test 命令来执行测试。 例如,您可以通过运行以下命令来运行为运行时创建的所有测试:

cargo test

有关使用 Rust cargo test 命令和测试框架的更多信息,请运行以下命令:

cargo help test

在模拟运行时中测试 pallet 日志

除了可以使用 Rust 测试框架进行的单元测试之外,您还可以通过构建模拟运行时环境来验证运行时中的逻辑。 配置类型 Test 定义为 Rust 枚举,其中包含模拟运行时中使用的每个 pallet 配置特性的实现。

frame_support::construct_runtime!(
 pub enum Test where
  Block = Block,
  NodeBlock = Block,
  UncheckedExtrinsic = UncheckedExtrinsic,
 {
  System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
  TemplateModule: pallet_template::{Pallet, Call, Storage, Event<T>},
 }
);

impl frame_system::Config for Test {
 // -- snip --
 type AccountId = u64;
}

如果 Test 实现 pallet_balances::Config,则赋值可能会对 Balance 类型使用 u64。 例如:

impl pallet_balances::Config for Test {
 // -- snip --
 type Balance = u64;
}

通过将 pallet_balances::Balanceframe_system::AccountId 分配给 u64,测试帐户和余额只需要在模拟运行时中跟踪 (AccountId: u64, Balance: u64) 映射。

在模拟运行时中测试存储

sp-io crate 公开了 TestExternalities 实现,您可以使用它在模拟环境中测试存储。 它是 substrate_state_machine 中基于内存的哈希映射外部函数实现的类型别名,称为 TestExternalities

以下示例演示了定义名为 ExtBuilder 的结构体以构建 TestExternalities 实例,并将区块号设置为 1。

pub struct ExtBuilder;

impl ExtBuilder {
 pub fn build(self) -> sp_io::TestExternalities {
  let mut t = system::GenesisConfig::default().build_storage::<TestRuntime>().unwrap();
  let mut ext = sp_io::TestExternalities::new(t);
  ext.execute_with(|| System::set_block_number(1));
  ext
 }
}

要在单元测试中创建测试环境,会调用 build 方法以使用默认创世配置生成 TestExternalities

#[test]
fn fake_test_example() {
 ExtBuilder::default().build_and_execute(|| {
  // ...测试逻辑...
 });
}

Externalities 的自定义实现允许您构建提供对外部节点功能的访问的运行时环境。 另一个示例可以在 offchain 模块中找到。 offchain 模块维护其自己的 Externalities 实现。

在模拟运行时中测试事件

除了存储之外,测试从链中发出的事件也很重要。 假设您使用 generate_deposit 宏使用 deposit_event 的默认生成,则所有 pallet 事件都存储在 system / events 键下,其中包含一些作为 EventRecord 的额外信息。

可以使用 System::events() 直接访问和迭代这些事件记录,但 system pallet 中还定义了一些辅助方法,可在测试中使用,即 assert_last_eventassert_has_event

fn fake_test_example() {
 ExtBuilder::default().build_and_execute(|| {
  System::set_block_number(1);
  // ...发出 FakeEvent1 然后是 FakeEvent2 的测试逻辑...
  System::assert_has_event(Event::FakeEvent1{}.into())
  System::assert_last_event(Event::FakeEvent2 { data: 7 }.into())
  assert_eq!(System::events().len(), 2);
 });
}

需要注意的是:

  • 创世块不会发出事件,因此需要设置区块号才能通过此测试。
  • 实例化 pallet 事件后,您需要有一个 .into(),这会将其转换为通用事件。

高级事件测试

在测试 pallet 中的事件时,通常您只对 pallet 自己发出的事件感兴趣。 以下辅助函数会过滤事件,只包含 pallet 发出的事件,并将它们转换为自定义事件类型。 此类辅助函数通常放在 mock.rs 文件中,用于在模拟运行时中进行测试。

fn only_example_events() -> Vec<super::Event<Runtime>> {
 System::events()
  .into_iter()
  .map(|r| r.event)
  .filter_map(|e| if let RuntimeEvent::TemplateModule(inner) = e { Some(inner) } else { None })
  .collect::<Vec<_>>();
}

此外,如果您的测试执行按顺序发出事件的操作,您可能只想查看自上次检查以来发生的事件。 以下示例利用了前面的辅助函数。

parameter_types! {
 static ExamplePalletEvents: u32 = 0;
}

fn example_events_since_last_call() -> Vec<super::Event<Runtime>> {
 let events = only_example_events();
 let already_seen = ExamplePalletEvents::get();
 ExamplePalletEvents::set(events.len() as u32);
 events.into_iter().skip(already_seen as usize).collect()
}

您可以在 提名池staking 的测试中找到此类事件测试的示例。 如果您使用此新函数重写之前的事件测试,则生成的代码如下所示:

fn fake_test_example() {
 ExtBuilder::default().build_and_execute(|| {
  System::set_block_number(1);
  // ...发出 FakeEvent1 的测试逻辑...
  assert_eq!(
   example_events_since_last_call(),
   vec![Event::FakeEvent1{}]
  );
  // ...发出 FakeEvent2 的测试逻辑...
  assert_eq!(
   example_events_since_last_call(),
   vec![Event::FakeEvent2{}]
  );
 });
}

创世配置

在前面的示例中,ExtBuilder::build() 方法使用默认创世配置来构建模拟运行时环境。 在许多情况下,在测试之前设置存储很方便。 例如,您可能希望在测试之前预先播种帐户余额。

frame_system::Config 的实现中,AccountIdBalance 都设置为 u64。 您可以将 (u64, u64) 对放入 balances vec 中,以将 (AccountId, Balance) 对作为帐户余额播种。 例如:

impl ExtBuilder {
 pub fn build(self) -> sp_io::TestExternalities {
  let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
  pallet_balances::GenesisConfig::<Test> {
   balances: vec![
    (1, 10),
    (2, 20),
    (3, 30),
    (4, 40),
    (5, 50),
    (6, 60)
   ],
  }
   .assimilate_storage(&mut t)
   .unwrap();

  let mut ext = sp_io::TestExternalities::new(t);
  ext.execute_with(|| System::set_block_number(1));
  ext
 }
}

在此示例中,帐户 1 的余额为 10,帐户 2 的余额为 20,依此类推。

用于定义 pallet 创世配置的确切结构取决于 pallet GenesisConfig 结构体定义。 例如,在 Balances pallet 中,它定义为:

pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
 pub balances: Vec<(T::AccountId, T::Balance)>,
}

区块生产

模拟区块生产对于验证预期行为在区块生产过程中是否成立很有用。

一种简单的方法是在所有模块的 on_initializeon_finalize 调用之间递增 System 模块的区块号,其中 System::block_number() 作为唯一的输入。 虽然对运行时代码来说缓存对存储或系统模块的调用很重要,但测试环境脚手架应优先考虑可读性,以方便将来的维护。

fn run_to_block(n: u64) {
 while System::block_number() < n {
  if System::block_number() > 0 {
   ExamplePallet::on_finalize(System::block_number());
   System::on_finalize(System::block_number());
  }
  System::reset_events();
  System::set_block_number(System::block_number() + 1);
  System::on_initialize(System::block_number());
  ExamplePallet::on_initialize(System::block_number());
 }
}

如果 pallet 特性实现了 frame_support::traits::{OnInitialize, OnFinalize} 特性以分别在每个区块之前和之后执行运行时方法中编码的逻辑,则仅从 ExamplePallet 调用 on_finalizeon_initialize 方法。

然后按以下方式调用此函数。

#[test]
fn my_runtime_test() {
 with_externalities(&mut new_test_ext(), || {
  assert_ok!(ExamplePallet::start_auction());
  run_to_block(10);
  assert_ok!(ExamplePallet::end_auction());
 });
}

接下来的步骤