Introducing multitest

Let me introduce the multitest - library for creating tests for smart contracts in Rust.

The core idea of multitest is abstracting an entity of contract and simulating 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 a multitest to our Cargo.toml.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }

[dev-dependencies]
cw-multi-test = "0.13.4"

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

When we have the dependency ready, update our test to use the framework:

use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_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_binary(&query::greet()?),
    }
}

#[allow(dead_code)]
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 the multitest purposes contract has to contain at least instantiate, query, and execute handlers. I attributed the function as #[allow(dead_code)], 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 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 would interact with 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 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 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 function is an accessor for querying Api (queries are handled a bit differently than other calls), and the query_wasm_smart 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.