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
.
[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:
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.