7-Contract Testing

Ink! supports three different stages of testing: unit, integration and end-to-end tests. On this page we'll explain what the purpose of each stage is about and how to use it.

We will use this example to illustrate the different tests:

#[ink(storage)] 
pub struct Incrementer { 
    value: i32,
    updates_by_user: Mapping<AccountId, u32>, 
}


#[ink(event)]
pub struct Incremented {
    by: i32,
    new_value: i32,
    who: AccountId,
}

impl Incrementer {
    #[ink(constructor)]
    pub fn new(init_value: i32) -> Self {
        Self {
            value: init_value,
            updates_by_user: Mapping::default(),
        }
    }

    #[ink(message)]
    pub fn inc(&mut self, by: i32) {
        // update the value
        self.value += by;
        // get the caller
        let signer = self.env().caller();
        // set the number of updates for this user
        let nb_updates = self.updates_by_user.get(&signer).unwrap_or(0);
        self.updates_by_user.insert(&signer, &(nb_updates + 1));
        // emit the event
        self.env().emit_event(Incremented {
            by,
            new_value: self.value,
            who: signer,
        });
    }

    #[ink(message)]
    pub fn get_value(&self) -> i32 {
        self.value
    }

    #[ink(message)]
    pub fn get_nb_updates(&self) -> u32 {
        // get the caller
        let signer = self.env().caller();
        self.updates_by_user.get(&signer).unwrap_or(0)
    }
}


Unit Tests

Testing contracts off-chain is done by cargo test and users can simply use the standard Rust routines of creating unit test modules within the ink! project.

#[test]
fn test_constructor() {
	let accounts = accounts();
	let contract = Incrementer::new(10);
	assert_eq!(10, contract.get_value());
}


Here we can test only the constructor because the other methods use the method self.env().caller() that is accessible only in a blockchain environment. Fortunately we have the integration tests on this purpose.

Integration Tests

For integration tests, the test is annotated with #[ink::test] attribute instead of #[test]. This attribute denotes that the test is then executed in a simulated, mocked blockchain environment. here are functions available to influence how the test environment is configured (e.g. setting a specified balance of an account to simulate how a contract would behave when interacting with it).

        #[ink::test]
        fn test() {
            let accounts = accounts();
            let mut contract = Incrementer::new(10);

            // alice increments the value
            change_caller(accounts.alice);
            assert_eq!(0, contract.get_nb_updates());
            contract.inc(3);
            assert_eq!(13, contract.get_value());
            assert_eq!(1, contract.get_nb_updates());

            // bob increments the value
            change_caller(accounts.bob);
            assert_eq!(0, contract.get_nb_updates());
            contract.inc(5);
            assert_eq!(18, contract.get_value());
            assert_eq!(1, contract.get_nb_updates());
        }


In this tests we use the methods openbrush::test_utils::accounts and , openbrush::test_utils::change_caller to change the connected account.

Integration Tests are well but there are some limitations like only the DefaultEnvironment is supported. It’s why we also have the e2e tests making it behave as close to the real chain environment as possible

End-to-End (E2E) Tests

E2E testing enables developers to write a test that will not only test the contract in an isolated manner; instead the contract will be tested together with all components that will be involved on-chain – so from end to end. This way of testing resembles closely how the contract will actually behave in production.

As part of the test, the contract will be compiled and deployed to a Substrate node that is running in the background. ink! offers API functions that enable developers to then interact with the contract via transactions that they create and submit to the blockchain.

You as a developer can define assertions on the outcome of their transactions, such as checking for state mutations, transaction failures or incurred gas costs.

Your chain configuration will be tested together with the smart contract. And if your chain has pallets that are involved with the smart contract execution, those will be part of the test execution as well.

ink! does not put any requirement on the Substrate node in the background – for example, you can run a node that contains a snapshot of a live network.

    #[ink_e2e::test]
    async fn test(mut client: ink_e2e::Client<C, E>) -> E2EResult<()> {
        // given
        let constructor = IncrementerRef::new(10);
        let contract_id = client
            .instantiate("incrementer", &ink_e2e::alice(), constructor, 0, None)
            .await
            .expect("instantiate failed")
            .account_id;

        // when alice increments the value
        let inc_value = build_message::<IncrementerRef>(contract_id.clone())
            .call(|contract| contract.inc(3));
        client
            .call(&ink_e2e::alice(), inc_value, 0, None)
            .await
            .expect("inc failed");

        // then : the value has been updated
        let get_value = build_message::<IncrementerRef>(contract_id.clone())
            .call(|contract| contract.get_value());
        let result = client
            .call_dry_run(&ink_e2e::alice(), &get_value, 0, None)
            .await;
        assert_eq!(13, result.return_value());

        // the number of updates for alice as well
        let get_nb_updates = build_message::<IncrementerRef>(contract_id.clone())
            .call(|contract| contract.get_nb_updates());
        let result = client
            .call_dry_run(&ink_e2e::alice(), &get_nb_updates, 0, None)
            .await;
        assert_eq!(1, result.return_value());

        Ok(())
    }

Before you can run the test, you have to install a Substrate node with pallet-contracts. By default e2e tests require that you install substrate-contracts-node.

To install the latest version:

cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git

You can also use Swanky node. The easiest method of installation is by downloading and executing a precompiled binary from the Release Page.

And you need to change CONTRACTS_NODE environment variable:

export CONTRACTS_NODE="YOUR_CONTRACTS_NODE_PATH"

You do not need to run it in the background since the node is started for each test independently.

And finally execute the following command to start e2e test execution.

cargo test --features e2e-tests

Conclusion

Writing tests is very important in all computer systems, and even more with the smart contracts in the blockchain. E2E testing enables developers to write tests that will not only test the contract in an isolated manner; instead, the contract will be tested together, it can simulate closely how the contract will actually behave in production.

Git repository

https://github.com/GuiGou12358/astar-tutorials/tree/main/tuto7

Source

https://use.ink/basics/contract-testing/

0
GuiGouPost author

Crypto-enthusiast, Defi & NFT believer, Dotsam Fam Astar Tech Amb & Phala Amb Web2 builder gradually migrating to Web3

Tutorials to write Smart Contracts in Rust and Ink!

0 comments

Tutorials to write Smart Contracts in Rust and Ink!