Multitest introduction

Introducing multitest

Let me introduce MultiTest - a library for creating tests for smart contracts in Rust.

The core idea of MultiTest is to abstract smart contracts and simulate the blockchain environment for testing purposes. The purpose of this is to be able to test communication between smart contracts. It does its job well, but it is also an excellent tool for testing single-contract scenarios.

First, we need to add MultiTest to our Cargo.toml.

Cargo.toml
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
 
[lib]
crate-type = ["cdylib"]
 
[dependencies]
cosmwasm-std = { version = "2.1.4", features = ["staking"] }
serde = { version = "1.0.214", default-features = false, features = ["derive"] }
 
[dev-dependencies]
cw-multi-test = "2.2.0"

I added a new [dev-dependencies] (opens in a new tab) section with dependencies not used by the final binary but which may be used by tools around the development process - for example, tests.

Once the dependency is there, let's update our test to use the framework:

src/contract.rs
use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
 
pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}
 
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;
 
    match msg {
        Greet {} => to_json_binary(&query::greet()?),
    }
}
 
pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult<Response> {
    unimplemented!()
}
 
mod query {
    use super::*;
 
    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };
 
        Ok(resp)
    }
}
 
#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};
 
    use super::*;
 
    #[test]
    fn greet_query() {
        let mut app = App::default();
 
        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));
 
        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &Empty {},
                &[],
                "Contract",
                None,
            )
            .unwrap();
 
        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();
 
        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

You probably notice that I added the function for an execute entry point. I didn't add the entry point itself or the function's implementation, but for multitest purposes, the contract has to contain at least instantiate, query, and execute handlers. I attributed the function as #[allow(dead_code)] (opens in a new tab), so, cargo will not complain about it not being used anywhere. Enabling it for tests only with #[cfg(test)] would also be a way.

Then, at the beginning of the test, I created the App (opens in a new tab) object. It is a core multitest entity representing the virtual blockchain on which we will run our contracts. As you can see, we can call functions on it just like we could when interacting with a blockchain using wasmd!

Right after creating app, I prepared the representation of the code, which would be "uploaded" to the blockchain. As multitests are just native Rust tests, they do not involve any Wasm binaries, but this name matches well what happens in a real-life scenario. We store this object in the blockchain with the store_code (opens in a new tab) function, and as a result, we are getting the code id - we would need it to instantiate a contract.

Instantiation is the next step. In a single instantiate_contract (opens in a new tab) call, we provide everything we would provide via wasmd - the contract code id, the address which performs instantiation,

the message triggering it, and any funds sent with the message (again - empty for now). We are adding the contract label and its admin for migrations - None, as we don't need it yet.

And after the contract is online, we can query it. The wrap (opens in a new tab) function is an accessor for querying Api (queries are handled a bit differently than other calls), and the query_wasm_smart (opens in a new tab) queries are given a contract with the message. Also, we don't need to care about query results as Binary - multitest assumes that we would like to deserialize them to some response type, so it takes advantage of Rust type elision to provide us with a nice Api.

Now it's time to rerun the test. It should still pass, but now we nicely abstracted the testing contract as a whole, not some internal functions. The next thing we should probably cover is making the contract more interesting by adding some state.