Testing
Eventually, testing becomes a crucial component of the Smart Contract development process. Tests ensure that modifications can be quickly integrated into the contract codebase without causing disruptions to other elements.
Generally, a well-designed set of contracts will have a comprehensive set of tests divided into two categories: Unit Testing and Integration Testing.
Unit testing
For an overview on unit testing, see Testing.
Integration testing with cw-multi-test
The cw-multi-test package offered in the cw-plus repository provides an interesting way to test your smart contracts without having to fully deploy them onto a testnet. Before the availability of the multi-test package, the typical workflow involved having a pipeline that would set up the contracts on a specified chain (such as a testnet or local network), perform tests, and then, if possible, destroy or self-destruct those contracts.
cw-multi-test concepts
There are a few key concepts that you need to understand before you can simulate a blockchain environment in Rust and run tests that involve contract-to-contract and contract-to-bank interactions.
In this section, we will take a step-by-step approach in writing a test with cw-multi-test, while explaining some important concepts along the way.
To begin, we need a sample contract, such as the cw-template, which is a simple boilerplate contract with two functions: Increment and Reset.
We start by creating a new test file with the following imports:
use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR};
use cw_multi_test::{App, BankKeeper, Contract, ContractWrapper};
The above imports provide us with a wide range of tools for creating a test. The first import to consider is App, which will serve as the simulated blockchain environment in which our tests will be run.
App
The main entry point to the system is called App, which represents a blockchain application. It keeps track of the block height and time, which you can modify to simulate multiple blocks. You can use app.update_block(next_block) to increment the timestamp by 5s and height by 1 (simulating a new block), or you can write any other mutator to advance it further.
The App provides the App.execute entry point that allows you to execute any CosmosMsg message and wraps it as an atomic transaction. If execute returns success, then the state will be committed. It returns the data and a list of events on successful execution or an Err(String) in case of an error. There are some helper methods associated with the Executor trait that create the CosmosMsg for you, offering a less verbose API. The instantiate_contract, execute_contract, and send_tokens methods are provided for your convenience in writing tests. Each of these methods executes one CosmosMsg message atomically as if it was submitted by a user. If you want to run multiple messages together and revert all state if any fail, you can also use the execute_multi method.
The other key entry point to App is the Querier interface that it implements. In particular, you can use App.wrap() to get a QuerierWrapper, which provides all kinds of nice APIs to query the blockchain, like all_balances and query_wasm_smart. Putting this together, you have one Storage wrapped into an application, where you can execute contracts and bank, query them easily, and update the current BlockInfo, in an API that is not very verbose or cumbersome. Under the hood, it will process all messages returned from contracts, move "bank" tokens, and call into other contracts.
The other important aspect of App is the Querier interface that it implements. You can use the App.wrap() method to obtain a QuerierWrapper, which provides various convenient APIs for querying the blockchain, such as all_balances and query_wasm_smart. This allows you to easily access the Storage and execute contracts, banks, and queries. It also enables you to update the current BlockInfo in an API that is straightforward and user-friendly. Internally, it handles all messages returned from contracts, transfers "bank" tokens, and invokes other contracts.
You can create an App for use in your test code as follows:
fn mock_app() -> App {
let env = mock_env();
let api = Box::new(MockApi::default());
let bank = BankKeeper::new();
App::new(api, env.block, bank, Box::new(MockStorage::new()))
}
Mocking contracts
Mocking your contracts is one of the mantras of multi-testing, but it also presents one of the main obstacles to a successful test. To begin with, any contract you aim to test must be either mocked or wrapped. The cw-multi-test offers a ContractWrapper that enables you to encapsulate the logical components of your contract, such as instantiation, execution, and queries, and deploy it to a mocked network.
Mocking all your contracts and then testing each one can be accomplished in a scripting style, but for the sake of maintainability, it is recommended to define all your wrapped contracts as functions so that you can reuse them:
use crate::contract::{execute, instantiate, query, reply};
pub fn contract_stablecoin_exchanger() -> Box<dyn Contract<Empty>>{
let contract = ContractWrapper::new_with_empty(
execute,
instantiate,
query,
).with_reply(reply);
Box::new(contract)
}
The above is a more complex example, but let's break it down quickly. We import the execute, instantiate, Query, and Reply functions, which are used at runtime by the contract, and then create our wrapper for use in the tests.
::alert{variant="info"} To reply or not to reply: That is the question.
Depending on the structure of your contract, when creating a ContractWrapper, you may not need with_reply if your contract does not have an implemented Reply function. #title Info ::
After mocking a contract, two more steps will follow: storing the code and then setting up a contract from the code object. You will notice that this is the exact same process for deploying to a testnet or mainnet chain. In unit tests, you work with a mocked_env, using mock_dependencies and passing in mock_info.
Storing and instantiating a contract:
Before a contract can be instantiated in a cw-multi-test environment, it first has to be stored. Once stored, the contract can be instantiated using its associated code_id.
let contract_code_id = router.store_code(contract_stablecoin_exchanger());
Instantiating from the new code object:
let mocked_contract_addr = router
.instantiate_contract(contract_code_id, owner.clone(), &msg, &[], "super-contract", None)
.unwrap();
All of the above gives you one mocked contract. As you start testing, you may encounter errors like:
- No ContractData
- Contract <contract> does not exist
If you encounter any of these, there's a good chance you're missing a mock. In the cw-multi-test environment, everything that can be considered a contract, or anything that your contract interacts with, needs to be mocked out. This includes your own simple utility contract and any services it interacts with, even if you don't intend to test it immediately.
Look at your contract and see if you are passing any dummy contract addresses, as this is the most likely cause. If you find any, you must: mock it out using the method described above, instantiate it using the method described above, capture its address, and pass that instead of a dummy one. Now, let's move on to the next major issue: mocking other services.
::alert{variant="info"} An address must be in lowercase alphanumeric form to be considered valid. For example, "owner" is valid, but "OWNER" is not. #title Info ::
Putting it all together
use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR};
use cw_multi_test::{App, BankKeeper, Contract, ContractWrapper};
use crate::contract::{execute, instantiate, query, reply};
use crate::msg::{InstantiateMsg, QueryMsg}
fn mock_app() -> App {
let env = mock_env();
let api = Box::new(MockApi::default());
let bank = BankKeeper::new();
App::new(api, env.block, bank, Box::new(MockStorage::new()))
}
pub fn contract_counter() -> Box<dyn Contract<Empty>>{
let contract = ContractWrapper::new_with_empty(
execute,
instantiate,
query,
);
Box::new(contract)
}
pub fn counter_instantiate_msg(count: Uint128) -> InstantiateMsg {
InstantiateMsg {
count: count
}
}
#[test]
fn counter_contract_multi_test() {
// Create the owner account
let owner = Addr::unchecked("owner");
let mut router = mock_app();
let counter_contract_code_id = router.store_code(contract_counter());
// Setup the counter contract with an initial count of zero
let init_msg = InstantiateMsg {
count: Uint128::zero()
}
// Instantiate the counter contract using its newly stored code id
let mocked_contract_addr = router
.instantiate_contract(counter_contract_code_id, owner.clone(), &init_msg, &[], "counter", None)
.unwrap();
// We can now start executing actions on the contract and querying it as needed
let msg = ExecuteMsg::Increment {}
// Increment the counter by executing the prepared msg above on the previously setup contract
let _ = router.execute_contract(
owner.clone(),
mocked_contract_addr.clone(),
&msg,
&[],
)
.unwrap();
// Query the contract to verify the counter was incremented
let config_msg = QueryMsg::Count{};
let count_response: CountResponse = router
.wrap()
.query_wasm_smart(mocked_contract_addr.clone(), &config_msg)
.unwrap();
asserteq!(count_response.count, 1)
// Now let's reset the counter with the other ExecuteMsg
let msg = ExecuteMsg::Reset {}
let _ = router.execute_contract(
owner.clone(),
mocked_contract_addr.clone(),
&msg,
&[],
)
.unwrap();
// And again use the available contract query to verify the result
// Query the contract to verify the counter was incremented
let config_msg = QueryMsg::Count{};
let count_response: CountResponse = router
.wrap()
.query_wasm_smart(mocked_contract_addr.clone(), &config_msg)
.unwrap();
asserteq!(count_response.count, 0)
}
Mocking 3rd party contracts
If you have read the above section, you should have a general understanding of the amount of work required to mock out your contracts. As you continue to mock and test, you may come across a roadblock when you realize that your contracts interact with Terraswap, Anchor, or some other service in the IBC. But don't worry, it's not a big problem.
You may start trying to mock one of these services in the same way as described above, only to realize that you need access to the code. The contract code is what you import to obtain execute, instantiate, and Query. However, you may then notice that protocols don't include their contract code in their Rust packages; they only include what you need to interact with them, such as messages and some helpers.
However, all hope is not lost. You can still make progress by trying to create a thin mock of the service you interact with. The process is similar to mocking your contracts (as described above), except you will need to implement all the functionality. This task is made easier because you can create a smaller ExecuteMsg with only the function you use, or a mock Query handler with only the queries you need. Here is an example of our mock for a third-party contract:
pub fn contract_ping_pong_mock() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(
|deps, _, info, msg: MockExecuteMsg| -> StdResult<Response> {
match msg {
MockExecuteMsg::Receive(Cw20ReceiveMsg {
sender: _,
amount: _,
msg,
}) => {
let received: PingMsg = from_binary(&msg)?;
Ok(Response::new()
.add_attribute("action", "pong")
.set_data(to_binary(&received.payload)?))
}
}})}
|_, _, msg: MockQueryMsg| -> StdResult<Binary> {
match msg {
MockQueryMsg::Pair {} => Ok(to_binary(&mock_pair_info())?),
When defining your own mocked contract, you have a lot of flexibility. You can omit dependencies, environment, and information variables by using _ if they are not used, and you can return any desired responses for a given ExecuteMsg or Query. The challenge then becomes how to mock all these services.