Introducing MultiTest
Let's introduce MultiTest, a library for testing 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, ant it is also an excellent tool for testing single-contract scenarios.
First, we need to add MultiTest dependency to our Cargo.toml
.
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
cosmwasm-std = { version = "3", features = ["staking"] }
serde = { version = "1.0.214", default-features = false, features = ["derive"] }
[dev-dependencies]
cw-multi-test = "3"
We have 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.
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've probably noticed that we have added the function for an execute
entry point. We didn't add the
entrypoint itself or the function's implementation, but for multitest purposes, the contract has to
contain at least instantiate, query, and execute handlers. We 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, we 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 could when interacting with a blockchain using
wasmd
!
Right after creating app
, we prepared the representation of the code
, which would be "uploaded"
to the blockchain. As multi-tests 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.