Test a transfer function

⚠️ Update Notice:

Please read Substrate to Polkadot SDK page first.


Testing each function is an important part of developing pallets for production. This guide steps you through best practices for writing test cases for a basic transfer function.

Outline the transfer function

A transfer function has two key elements: subtracting a balance from an account and adding that balance to another account. Here, we'll start by outlining this function:

#[pallet::weight(10_000)]
pub (super) fn transfer(
  origin: OriginFor<T>,
  to: T::AccountId,
  #[pallet::compact] amount: T::Balance,
  ) -> DispatchResultWithPostInfo {
    let sender = ensure_signed(origin)?;

    Accounts::<T>::mutate(&sender, |bal| {
      *bal = bal.saturating_sub(amount);
      });
      Accounts::<T>::mutate(&to, |bal| {
        *bal = bal.saturating_add(amount);
        });

    Self::deposit_event(Event::<T>::Transferred(sender, to, amount))
    Ok(().into())
}

Check that the sender has enough balance

The first thing to verify, is whether the sender has enough balance. In a separate tests.rs file, write out this first test case:

#[test]
fn transfer_works() {
  new_test_ext().execute_with(|| {
    MetaDataStore::<Test>::put(MetaData {
			issuance: 0,
			minter: 1,
			burner: 1,
		});
    // Mint 42 coins to account 2.
    assert_ok!(RewardCoin::mint(RuntimeOrigin::signed(1), 2, 42));
    // Send 50 coins to account 3.
    asset_noop!(RewardCoin::transfer(RuntimeOrigin::signed(2), 3, 50), Error::<T>::InsufficientBalance);

Configure error handling

To implement some error check, replace mutate with try_mutate to use ensure!. This will check whether bal is greater or equal to amount and throw an error message if not:

Accounts::<T>::try_mutate(&sender, |bal| {
  ensure!(bal >= amount, Error::<T>::InsufficientBalance);
  *bal = bal.saturating_sub(amount);
  Ok(())
});

Run cargo test from your pallet's directory.

Check that sending account doesn't go below minimum balance

Inside your transfer_works function:

assert_noop!(RewardCoin::transfer(RuntimeOrigin::signed(2), 3, 50), Error::<Test>::InsufficientBalance);

Check that both tests work together

Use #[transactional] to generate a wrapper around both checks:

#[transactional]
pub(super) fn transfer(
/*--snip--*/

Handle dust accounts

Make sure that sending and receiving accounts aren't dust accounts. Use T::MinBalance::get():

/*--snip--*/
let new_bal = bal.checked_sub(&amount).ok_or(Error::<T>::InsufficientBalance)?;
ensure!(new_bal >= T::MinBalance::get(), Error::<T>::BelowMinBalance);
/*--snip--*/

Examples

Resources