0

This question is more about best practices, and if there is a better method for providing mock data for off-chain ink! tests.

The goal is to use mock data for off-chain unit testing for ink! contracts. Cross-contract calls do not work in an off-chain environment. In this example, the contract performs a cross-contract call to an ERC-20 contract to get a caller's balance. My solution was to implement conditional compiling to "re-route" a helper function.

//only compiles when *not* running tests
#[cfg(not(test))]
fn get_token_balance(&self, caller: &AccountId) -> Balance {
    //calls the external ERC-20 contract
    self.token.balance_of(*caller)
}

//only compiles when running tests
#[cfg(test)]
fn get_token_balance(&self, _: &AccountId) -> Balance {
    //arbitrary value
    1
}

When compiling for tests, the function that contains the cross-contract call will be excluded. Instead, a different version of the function will be compiled returning some arbitrary value, enabling the unit tests to run.

The general pattern would be:

  • Extrapolate cross-contract calls into a helper function
  • Add a configuration to the function that performs the cross-contract call to not compile when building tests
  • Add another function that does compile when building in tests. This function returns any mock data that would be returned from the external contract.

One limitation of this method is that it will only work for cross-contract calls that do not perform writes but only reads. One solution to this is adding an extra field to the contract that only compiles with tests. This field will work as a fake-storage.

#[ink(storage)]
pub struct MyContract {
    #[cfg(test)]
    mock_field: SomeStruct, // will serve as a fake storage
}

...

//on-chain, performs cross-contract call
#[cfg(not(test))]
fn do_some_write(&mut self) {
    self.external_contract.write_to_field(0xDEADBEEF);
}


//testing environment only
#[cfg(test)]
fn do_some_write(&mut self) {
    self.mock_field.my_fake_storage_item = 0xDEADBEEF;
}

Is there a more streamlined approach to providing these mock tests? Or this a viable solution that future ink! developers could use to make their unit tests pass?

1 Answer 1

2

Testing cross-contract calls is the biggest missing piece for developer experience in ink! 3.0 right now.

There are ways to do it currently, but none of them is ideal. The concept of using feature flags you used works, but it's complex and test logic ends up being mixed in with the actual contract. Another option is to use redspot, though I haven't tried this myself.

In the foreseeable future (1-2 months) we will introduce a proper solution with ink! 4.0:

  • The off-chain testing engine will be extended to support cross-contract unit testing. There is already a PR for it.
  • We're introducing a new attribute #[ink::e2e_test] that will interact with a real node process in the background and expose an API to interact with it (for deploying and calling contracts). Then you can test cross-contract calls in the real on-chain environment. There is this ticket for it and we're actively working on it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.